Chapter 2: NumPy Basics

📖 Reading Time: 20-25 min 💻 Code Examples: 10 📝 Exercises: 5 📊 Difficulty: Introductory

Master NumPy, the high-performance numerical computing library

Introduction

NumPy (Numerical Python) is the foundational library for high-speed numerical computing in Python. Machine learning requires processing large amounts of data efficiently, and NumPy is an essential tool for that purpose.

In this chapter, you will learn the following:

Why do we need NumPy?
Compared to Python lists, NumPy arrays are 10 to 100 times faster. In large-scale data processing, this speed difference becomes critical.

1. Creating NumPy Arrays

1.1 Basic Array Creation

Example 1: How to Create Arrays

import numpy as np

# Create an array from a list
arr1 = np.array([1, 2, 3, 4, 5])
print("1D array:", arr1)
print("Type:", type(arr1))
print("Data type:", arr1.dtype)
# Output:
# 1D array: [1 2 3 4 5]
# Type: <class 'numpy.ndarray'>
# Data type: int64

# 2D array (matrix)
arr2 = np.array([[1, 2, 3], [4, 5, 6]])
print("\n2D array:")
print(arr2)
print("Shape:", arr2.shape)  # (2, 3) = 2 rows, 3 columns
print("Number of dimensions:", arr2.ndim)
print("Number of elements:", arr2.size)
# Output:
# 2D array:
# [[1 2 3]
#  [4 5 6]]
# Shape: (2, 3)
# Number of dimensions: 2
# Number of elements: 6

# Specify the data type
arr3 = np.array([1.5, 2.3, 3.7], dtype=np.float32)
print("\nfloat32 array:", arr3)
print("Data type:", arr3.dtype)

1.2 Convenient Array Creation Functions

Example 2: Creating Special Arrays

import numpy as np

# Array filled with zeros
zeros = np.zeros((3, 4))  # 3 rows, 4 columns
print("Zero array:")
print(zeros)

# Array filled with ones
ones = np.ones((2, 3))
print("\nOnes array:")
print(ones)

# Consecutive values
arange = np.arange(0, 10, 2)  # From 0 to less than 10, step 2
print("\narange:", arange)  # [0 2 4 6 8]

# Evenly spaced values
linspace = np.linspace(0, 1, 5)  # 5 values from 0 to 1
print("linspace:", linspace)  # [0.   0.25 0.5  0.75 1.  ]

# Identity matrix
identity = np.eye(3)  # 3x3 identity matrix
print("\nIdentity matrix:")
print(identity)

# Random array
np.random.seed(42)  # Seed for reproducibility
random = np.random.rand(2, 3)  # Uniform distribution over 0-1
print("\nRandom array:")
print(random)

# Random numbers following a normal distribution
normal = np.random.randn(3, 3)  # Mean 0, standard deviation 1
print("\nNormal distribution:")
print(normal)
graph LR A[Array Creation] --> B[np.array] A --> C[np.zeros/ones] A --> D[np.arange/linspace] A --> E[np.random] B --> F[Convert from list] C --> G[Initialize with specific value] D --> H[Generate sequence] E --> I[Generate random numbers] style A fill:#e3f2fd style F fill:#fff3e0 style G fill:#f3e5f5 style H fill:#e8f5e9 style I fill:#fce4ec

2. Array Shape Manipulation

Example 3: reshape, flatten, transpose

import numpy as np

# Original array
arr = np.arange(12)
print("Original array:", arr)  # [ 0  1  2  3  4  5  6  7  8  9 10 11]
print("Shape:", arr.shape)  # (12,)

# reshape: change the shape
reshaped = arr.reshape(3, 4)  # Reshape into 3 rows, 4 columns
print("\nreshape (3, 4):")
print(reshaped)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

# Automatic calculation using -1
reshaped2 = arr.reshape(2, -1)  # 2 rows, number of columns computed automatically
print("\nreshape (2, -1):")
print(reshaped2)  # Becomes 2 rows, 6 columns

# flatten: convert to a 1D array
flattened = reshaped.flatten()
print("\nflatten:", flattened)
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

# transpose: swap rows and columns
transposed = reshaped.T
print("\ntranspose:")
print(transposed)
# [[ 0  4  8]
#  [ 1  5  9]
#  [ 2  6 10]
#  [ 3  7 11]]

# Swap axes of a multidimensional array
arr3d = np.arange(24).reshape(2, 3, 4)
print("\nShape of 3D array:", arr3d.shape)  # (2, 3, 4)
swapped = np.swapaxes(arr3d, 0, 2)
print("After swapping axes:", swapped.shape)  # (4, 3, 2)

3. Indexing and Slicing

Example 4: Accessing Array Elements

import numpy as np

# 1D array
arr1d = np.array([10, 20, 30, 40, 50])
print("Array:", arr1d)
print("arr1d[0]:", arr1d[0])    # 10
print("arr1d[-1]:", arr1d[-1])  # 50 (last element)
print("arr1d[1:4]:", arr1d[1:4])  # [20 30 40]

# 2D array
arr2d = np.array([[1, 2, 3, 4],
                  [5, 6, 7, 8],
                  [9, 10, 11, 12]])
print("\n2D array:")
print(arr2d)

# Element access
print("arr2d[0, 0]:", arr2d[0, 0])  # 1
print("arr2d[1, 2]:", arr2d[1, 2])  # 7 (row 2, column 3)

# Retrieve a row
print("First row:", arr2d[0])        # [1 2 3 4]
print("All rows, column 2:", arr2d[:, 1])  # [ 2  6 10]

# Slicing
print("Sub-array:")
print(arr2d[0:2, 1:3])
# [[2 3]
#  [6 7]]

# Boolean indexing
mask = arr2d > 5
print("\nMask for elements greater than 5:")
print(mask)
print("Elements greater than 5:", arr2d[mask])
# [ 6  7  8  9 10 11 12]

# Replace elements that satisfy a condition
arr_copy = arr2d.copy()
arr_copy[arr_copy > 5] = 0
print("\nSet elements greater than 5 to 0:")
print(arr_copy)
graph TD A[Indexing] --> B[Single element] A --> C[Slice] A --> D[Boolean indexing] B --> E["arr[i, j]"] C --> F["arr[1:3, :]"] D --> G["arr[arr > 5]"] style A fill:#e3f2fd style E fill:#fff3e0 style F fill:#f3e5f5 style G fill:#e8f5e9

4. Universal Functions

Example 5: Mathematical Operations

import numpy as np

# Create arrays
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])

# Basic operations (element-wise)
print("a + b =", a + b)      # [11 22 33 44]
print("a - b =", a - b)      # [-9 -18 -27 -36]
print("a * b =", a * b)      # [10 40 90 160]
print("a / b =", a / b)      # [0.1 0.1 0.1 0.1]
print("a ** 2 =", a ** 2)    # [ 1  4  9 16]

# Mathematical functions
arr = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])
print("\nTrigonometric functions:")
print("sin:", np.sin(arr))
print("cos:", np.cos(arr))
print("tan:", np.tan(arr))

# Exponential and logarithmic functions
x = np.array([1, 2, 3, 4])
print("\nExponential and logarithmic:")
print("exp(x):", np.exp(x))       # e^x
print("log(x):", np.log(x))       # Natural logarithm
print("log10(x):", np.log10(x))   # Common logarithm
print("sqrt(x):", np.sqrt(x))     # Square root

# Other functions
y = np.array([-2.5, -1.5, 0.5, 1.5, 2.5])
print("\nOthers:")
print("abs(y):", np.abs(y))       # Absolute value
print("ceil(y):", np.ceil(y))     # Round up
print("floor(y):", np.floor(y))   # Round down
print("round(y):", np.round(y))   # Round to nearest

# Maximum and minimum
print("\nMax and min:")
print("max:", np.max(x))          # 4
print("min:", np.min(x))          # 1
print("argmax:", np.argmax(x))    # 3 (index of the maximum value)
print("argmin:", np.argmin(x))    # 0 (index of the minimum value)

5. Broadcasting

Broadcasting is a powerful NumPy feature that performs operations between arrays of different shapes.

Example 6: Broadcasting Examples

import numpy as np

# Operations with a scalar
arr = np.array([1, 2, 3, 4])
print("arr + 10 =", arr + 10)  # [11 12 13 14]
print("arr * 2 =", arr * 2)    # [2 4 6 8]

# Operations between 1D and 2D
matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])
vector = np.array([10, 20, 30])

result = matrix + vector
print("\nMatrix + vector:")
print(result)
# [[11 22 33]
#  [14 25 36]
#  [17 28 39]]

# Row vector and column vector
row = np.array([[1, 2, 3]])  # Shape: (1, 3)
col = np.array([[10], [20], [30]])  # Shape: (3, 1)

result2 = row + col
print("\nRow vector + column vector:")
print(result2)
# [[11 12 13]
#  [21 22 23]
#  [31 32 33]]

# Practical example: standardization (transform to mean 0, standard deviation 1)
data = np.array([[1, 2, 3],
                 [4, 5, 6],
                 [7, 8, 9]], dtype=float)
mean = data.mean(axis=0)  # Mean per column
std = data.std(axis=0)    # Standard deviation per column

normalized = (data - mean) / std
print("\nStandardization:")
print("Original data:")
print(data)
print("Mean:", mean)
print("Standard deviation:", std)
print("After standardization:")
print(normalized)
graph TD A[Broadcasting] --> B["Scalar (1,) + Array (n,)"] A --> C["Vector (n,) + Matrix (m,n)"] A --> D["Row (1,n) + Column (m,1)"] B --> E[Applied to all elements] C --> F[Applied to each row] D --> G[Expanded into a grid] style A fill:#e3f2fd style E fill:#fff3e0 style F fill:#f3e5f5 style G fill:#e8f5e9

6. Statistical Functions

Example 7: Computing Statistics

import numpy as np

# Create data
np.random.seed(42)
data = np.random.randn(100)  # 100 normal random numbers

# Basic statistics
print("Mean:", np.mean(data))
print("Median:", np.median(data))
print("Standard deviation:", np.std(data))
print("Variance:", np.var(data))
print("Minimum:", np.min(data))
print("Maximum:", np.max(data))
print("Range:", np.ptp(data))  # max - min

# Percentiles
print("\nQuartiles:")
print("25%:", np.percentile(data, 25))
print("50%:", np.percentile(data, 50))
print("75%:", np.percentile(data, 75))

# Statistics of a 2D array (specifying the axis)
matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

print("\nStatistics of the 2D array:")
print("Total sum:", np.sum(matrix))  # 45
print("Sum per column:", np.sum(matrix, axis=0))  # [12 15 18]
print("Sum per row:", np.sum(matrix, axis=1))  # [ 6 15 24]

print("\nMean per column:", np.mean(matrix, axis=0))  # [4. 5. 6.]
print("Mean per row:", np.mean(matrix, axis=1))  # [2. 5. 8.]

# Cumulative statistics
arr = np.array([1, 2, 3, 4, 5])
print("\nCumulative sum:", np.cumsum(arr))  # [ 1  3  6 10 15]
print("Cumulative product:", np.cumprod(arr))   # [  1   2   6  24 120]

7. Linear Algebra

Example 8: Matrix Operations

import numpy as np

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

# Element-wise product
print("Element-wise product (A * B):")
print(A * B)
# [[ 5 12]
#  [21 32]]

# Matrix product (dot product)
print("\nMatrix product (A @ B):")
print(A @ B)  # or np.dot(A, B)
# [[19 22]
#  [43 50]]

# Dot product of vectors
v1 = np.array([1, 2, 3])
v2 = np.array([4, 5, 6])
print("\nDot product of vectors:", np.dot(v1, v2))  # 32 = 1*4 + 2*5 + 3*6

# Transpose
print("\nTranspose of A:")
print(A.T)

# Inverse matrix
A_inv = np.linalg.inv(A)
print("\nInverse of A:")
print(A_inv)

# Verify that it produces the identity matrix
print("\nA @ A_inv (should be the identity matrix):")
print(A @ A_inv)

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

# Eigenvalues and eigenvectors
eigenvalues, eigenvectors = np.linalg.eig(A)
print("\nEigenvalues:", eigenvalues)
print("Eigenvectors:")
print(eigenvectors)

# Norm (magnitude of a vector)
v = np.array([3, 4])
print("\nL2 norm:", np.linalg.norm(v))  # 5.0 = sqrt(3^2 + 4^2)

8. Practical Example: Data Preprocessing

Example 9: Data Preprocessing for Machine Learning

import numpy as np

# Sample data (height, weight, age)
np.random.seed(42)
data = np.random.randn(100, 3) * 10 + [170, 60, 30]
print("Data shape:", data.shape)  # (100, 3)
print("First 5 rows:")
print(data[:5])

# 1. Basic statistics
print("\n=== Basic statistics ===")
print("Mean:", data.mean(axis=0))
print("Standard deviation:", data.std(axis=0))
print("Minimum:", data.min(axis=0))
print("Maximum:", data.max(axis=0))

# 2. Handling missing values (data containing NaN)
data_with_nan = data.copy()
data_with_nan[0, 0] = np.nan
data_with_nan[5, 1] = np.nan

print("\n=== Handling missing values ===")
print("Number of missing values:", np.isnan(data_with_nan).sum())

# Impute missing values with the mean
for col in range(data_with_nan.shape[1]):
    col_mean = np.nanmean(data_with_nan[:, col])
    data_with_nan[np.isnan(data_with_nan[:, col]), col] = col_mean

print("Number of missing values after imputation:", np.isnan(data_with_nan).sum())

# 3. Standardization (Z-score normalization)
mean = data.mean(axis=0)
std = data.std(axis=0)
normalized_data = (data - mean) / std

print("\n=== After standardization ===")
print("Mean:", normalized_data.mean(axis=0))  # Approximately 0
print("Standard deviation:", normalized_data.std(axis=0))  # Approximately 1

# 4. Min-max normalization (scale to 0-1)
min_vals = data.min(axis=0)
max_vals = data.max(axis=0)
min_max_scaled = (data - min_vals) / (max_vals - min_vals)

print("\n=== After min-max normalization ===")
print("Minimum:", min_max_scaled.min(axis=0))  # 0
print("Maximum:", min_max_scaled.max(axis=0))  # 1

Example 10: Manipulating Image Data

import numpy as np

# Data simulating an image (5x5 grayscale image)
image = np.array([
    [0, 50, 100, 150, 200],
    [50, 100, 150, 200, 250],
    [100, 150, 200, 250, 255],
    [150, 200, 250, 255, 255],
    [200, 250, 255, 255, 255]
], dtype=np.uint8)

print("Original image:")
print(image)
print("Shape:", image.shape)  # (5, 5)

# 1. Flipping the image
flipped_h = np.flip(image, axis=1)  # Horizontal flip
flipped_v = np.flip(image, axis=0)  # Vertical flip

print("\nHorizontal flip:")
print(flipped_h)

# 2. Rotating the image (90 degrees)
rotated = np.rot90(image)
print("\n90-degree rotation:")
print(rotated)

# 3. Cropping the image
cropped = image[1:4, 1:4]  # Crop the central 3x3
print("\nCrop (3x3):")
print(cropped)

# 4. Brightness adjustment
brightened = np.clip(image + 50, 0, 255).astype(np.uint8)
print("\nBrightness +50:")
print(brightened)

# 5. Contrast adjustment
contrast = np.clip(image * 1.5, 0, 255).astype(np.uint8)
print("\nContrast x1.5:")
print(contrast)

# 6. Simulating an RGB image (5x5x3)
rgb_image = np.random.randint(0, 256, (5, 5, 3), dtype=np.uint8)
print("\nShape of RGB image:", rgb_image.shape)  # (5, 5, 3)

# Mean per channel
print("R channel mean:", rgb_image[:, :, 0].mean())
print("G channel mean:", rgb_image[:, :, 1].mean())
print("B channel mean:", rgb_image[:, :, 2].mean())

Summary

In this chapter, you learned the basics of NumPy:

Next step: In Chapter 3, you will use this knowledge to learn data analysis with Pandas.

Exercises

Exercise 1: Array Manipulation

Problem: Create an array containing the numbers from 1 to 50 and reshape it into a 5-row, 10-column matrix. Then, compute the sum of each row.

# Solution example
import numpy as np

# Array from 1 to 50
arr = np.arange(1, 51)
print("Array:", arr)

# Reshape into 5 rows, 10 columns
matrix = arr.reshape(5, 10)
print("\n5x10 matrix:")
print(matrix)

# Sum of each row
row_sums = matrix.sum(axis=1)
print("\nSum of each row:", row_sums)
# Output: [ 55 155 255 355 455]

# Verification: sum of the first row
print("Verification:", sum(range(1, 11)))  # 55
Exercise 2: Boolean Indexing

Problem: From the 100 numbers between 0 and 99, extract the numbers that are multiples of 3 but not multiples of 5.

# Solution example
import numpy as np

# Array from 0 to 99
numbers = np.arange(100)

# Condition: multiple of 3 and not a multiple of 5
condition = (numbers % 3 == 0) & (numbers % 5 != 0)
result = numbers[condition]

print("Result:", result)
print("Count:", len(result))
# Output: [ 3  6  9 12 18 21 24 27 33 36 39 42 48 51 54 57 63 66 69 72 78 81 84 87 93 96 99]
# Count: 27
Exercise 3: Statistical Processing

Problem: Generate 1000 data points following a normal distribution with mean 50 and standard deviation 10, and compute the frequency for each histogram bin (bins: 0-20, 20-40, 40-60, 60-80, 80-100).

# Solution example
import numpy as np

# Generate normally distributed data
np.random.seed(42)
data = np.random.normal(50, 10, 1000)

# Statistics
print("Mean:", data.mean())
print("Standard deviation:", data.std())

# Histogram (frequency count)
bins = [0, 20, 40, 60, 80, 100]
hist, edges = np.histogram(data, bins=bins)

print("\nHistogram:")
for i in range(len(hist)):
    print(f"{edges[i]}-{edges[i+1]}: {hist[i]} items")

# Example output:
# 0-20: 22 items
# 20-40: 159 items
# 40-60: 638 items
# 60-80: 175 items
# 80-100: 6 items
Exercise 4: Matrix Operations

Problem: For the following matrix A, find (1) the inverse matrix, (2) the determinant, and (3) the eigenvalues. Also verify that A @ A_inv produces the identity matrix.

\[ A = \begin{bmatrix} 2 & 1 \\ 1 & 3 \end{bmatrix} \]

# Solution example
import numpy as np

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

# (1) Inverse matrix
A_inv = np.linalg.inv(A)
print("Inverse matrix:")
print(A_inv)

# (2) Determinant
det = np.linalg.det(A)
print("\nDeterminant:", det)  # 5.0

# (3) Eigenvalues
eigenvalues, eigenvectors = np.linalg.eig(A)
print("\nEigenvalues:", eigenvalues)
print("Eigenvectors:")
print(eigenvectors)

# Verification: A @ A_inv = I (identity matrix)
identity = A @ A_inv
print("\nA @ A_inv (identity matrix):")
print(identity)
print("Difference from the identity matrix:", np.allclose(identity, np.eye(2)))
Exercise 5: Image Data Processing

Problem: Generate a 10x10 random image data (0-255), then (1) make the entire image twice as bright, and (2) extract the central 5x5 region. Note that brightness must be constrained to the range 0-255.

# Solution example
import numpy as np

# Generate a random image
np.random.seed(42)
image = np.random.randint(0, 256, (10, 10), dtype=np.uint8)

print("Original image:")
print(image)
print("Average brightness:", image.mean())

# (1) Make twice as bright (constrained to 0-255)
brightened = np.clip(image * 2, 0, 255).astype(np.uint8)
print("\nTwice as bright:")
print(brightened)
print("Average brightness:", brightened.mean())

# (2) Extract the central 5x5 (indices 2:7)
center = image[2:7, 2:7]
print("\nCentral 5x5:")
print(center)
print("Shape:", center.shape)  # (5, 5)

Disclaimer