NumPy Tutorial with examples

 NumPy Complete Syllabus (Beginner to Advanced)



🟢 LEVEL 1: Beginner (Foundations)

📌 Module 1: Introduction to NumPy

  • What is NumPy?

  • Why NumPy is faster than Python lists

  • Installing NumPy

  • Importing (import numpy as np)

  • Understanding ndarray


📌 Module 2: Creating Arrays

  • np.array()

  • 1D, 2D, 3D arrays

  • np.zeros()

  • np.ones()

  • np.eye()

  • np.arange()

  • np.linspace()

  • np.random.rand()

  • np.random.randint()

Practice:

  • Create identity matrix

  • Generate random dataset


📌 Module 3: Array Attributes

  • shape

  • ndim

  • size

  • dtype

  • itemsize

  • nbytes


📌 Module 4: Indexing & Slicing

  • 1D indexing

  • 2D indexing

  • Negative indexing

  • Slicing

  • Step slicing

  • Boolean indexing

  • Fancy indexing

Practice:

  • Extract rows/columns

  • Filter values > 10


🟡 LEVEL 2: Intermediate (Core Power)


📌 Module 5: Array Operations

  • Arithmetic operations

  • Vectorized operations

  • Element-wise operations

  • Comparison operators

  • Logical operations


📌 Module 6: Broadcasting (Very Important)

  • Broadcasting rules

  • Shape compatibility

  • Examples with 1D + 2D

  • Common broadcasting errors

  • Real-world normalization example


📌 Module 7: Reshaping & Manipulating

  • reshape()

  • flatten()

  • ravel()

  • transpose()

  • swapaxes()

  • expand_dims()

  • squeeze()


📌 Module 8: Stacking & Splitting

  • vstack()

  • hstack()

  • concatenate()

  • split()

  • hsplit()

  • vsplit()


📌 Module 9: Mathematical Functions

  • np.sqrt()

  • np.exp()

  • np.log()

  • np.power()

  • np.abs()

  • Trigonometric functions


📌 Module 10: Statistical Functions

  • mean

  • median

  • std

  • var

  • min

  • max

  • argmin

  • argmax

  • Axis parameter

Practice:

  • Compute column-wise mean

  • Normalize dataset


🟠 LEVEL 3: Advanced (Performance & Linear Algebra)


📌 Module 11: Linear Algebra

  • Dot product

  • Matrix multiplication (@)

  • Determinant

  • Inverse

  • Eigenvalues

  • SVD

  • Norms


📌 Module 12: Random Module

  • Random seeds

  • Uniform distribution

  • Normal distribution

  • Sampling

  • Shuffle


📌 Module 13: Memory Management

  • View vs Copy

  • copy()

  • base attribute

  • Memory layout (C vs Fortran order)

  • Performance implications


📌 Module 14: Performance Optimization

  • Vectorization vs loops

  • Avoid Python loops

  • In-place operations

  • Efficient slicing

  • Time benchmarking


🔴 LEVEL 4: Expert (Under the Hood)


📌 Module 15: Data Types & Dtype System

  • int8, int16, float32, float64

  • Type casting

  • Structured arrays

  • Memory optimization


📌 Module 16: Advanced Indexing

  • Boolean masks

  • Fancy indexing

  • Broadcasting with indexing

  • Multi-dimensional indexing tricks


📌 Module 17: Custom Functions on Arrays

  • np.apply_along_axis()

  • np.vectorize()

  • Universal functions (ufuncs)


📌 Module 18: NumPy Internals

  • How ndarray is stored in memory

  • Strides

  • Contiguous arrays

  • Why NumPy is fast

  • C backend overview


🧠 LEVEL 5: NumPy for Machine Learning


📌 Module 19: Data Preprocessing

  • Normalization

  • Standardization

  • Feature scaling

  • Handling missing values


📌 Module 20: Matrix Operations in ML

  • Linear regression using NumPy

  • Gradient descent implementation

  • Cost function computation

  • Vectorized loss calculation


📌 Module 21: Mini Projects

  • Implement Linear Regression from scratch

  • Implement K-Means

  • Image processing using NumPy

  • Build neural network forward pass


🎯 Interview Preparation Section

  • Broadcasting explanation

  • Why NumPy is fast?

  • Difference between reshape and resize

  • View vs copy

  • Axis parameter meaning

  • Dot vs matmul

  • Time complexity comparisons


📚 Suggested Learning Path (8-Week Plan)

Week 1–2: Beginner modules
Week 3–4: Intermediate
Week 5–6: Linear algebra & performance
Week 7: ML applications
Week 8: Projects + Interview prep



Perfect 👌
Let’s build 📌 Module 1: Introduction to NumPy in deep detail — beginner-friendly but technically strong.

You can directly use this as course content or notes.


📌 Module 1: Introduction to NumPy


1️⃣ What is NumPy?

NumPy stands for:

Numerical Python

It is the core library for numerical and scientific computing in Python.

NumPy provides:

  • Powerful N-dimensional arrays

  • Mathematical functions

  • Linear algebra operations

  • Statistical tools

  • Random number generation


🔥 Why NumPy Exists

Python lists are:

  • Flexible

  • Easy to use

  • But slow for heavy math

NumPy solves:

  • Speed issues

  • Memory inefficiency

  • Lack of vectorized math


2️⃣ Why NumPy is Faster than Python Lists

This is one of the most important concepts.


🔹 1. Homogeneous Data

Python list:

[1, "hello", 3.5]
  • Different data types

  • Stored as object references

  • Extra memory overhead

NumPy array:

np.array([1, 2, 3])
  • All elements must be same type

  • Stored in contiguous memory

  • Efficient memory usage


🔹 2. Stored in Contiguous Memory

Python list:

  • Stores pointers to objects

  • Objects stored separately in memory

NumPy:

  • Stores raw data in one continuous memory block

  • Much faster access


🔹 3. Written in C

NumPy operations are implemented in C under the hood.

Example:

import numpy as np
import time

size = 1_000_000

# Python list
a = list(range(size))
b = list(range(size))

start = time.time()
c = [x + y for x, y in zip(a, b)]
print("Python time:", time.time() - start)

# NumPy
a_np = np.arange(size)
b_np = np.arange(size)

start = time.time()
c_np = a_np + b_np
print("NumPy time:", time.time() - start)

NumPy is significantly faster.

Why?

Because:

  • Loop runs in C

  • No Python-level iteration


🔹 4. Vectorization

Instead of:

for i in range(len(a)):
    a[i] = a[i] * 2

You do:

a_np = a_np * 2

No loop. Cleaner. Faster.


3️⃣ Installing NumPy


🔹 Using pip

pip install numpy

🔹 Verify Installation

import numpy as np
print(np.__version__)

4️⃣ Importing NumPy

Standard convention:

import numpy as np

Why np?

  • Shorter to type

  • Industry standard

  • Makes code readable

Example:

import numpy as np

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

5️⃣ Understanding ndarray (Core of NumPy)

The heart of NumPy is:

ndarray = N-dimensional array


🔹 What is ndarray?

An ndarray is:

  • A grid of values

  • All values of the same type

  • Indexed by tuple of integers


🔹 Create Basic Array

import numpy as np

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

Output:

[1 2 3]

Type:

print(type(arr))

Output:

<class 'numpy.ndarray'>

🔹 Multi-Dimensional Array

2D Array

matrix = np.array([[1, 2], [3, 4]])
print(matrix)

3D Array

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

🔹 ndarray Important Properties

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

print(arr.shape)    # (2, 3)
print(arr.ndim)     # 2
print(arr.size)     # 6
print(arr.dtype)    # int64

🔹 shape

Returns dimensions:

(rows, columns)

🔹 ndim

Number of dimensions.

  • 1D → vector

  • 2D → matrix

  • 3D → tensor


🔹 size

Total elements.


🔹 dtype

Data type of elements.

Examples:

  • int32

  • int64

  • float32

  • float64


🔹 Memory Model of ndarray

Internally stores:

  • Data buffer (actual values)

  • Metadata:

    • Shape

    • Data type

    • Strides

You can check memory size:

print(arr.nbytes)

🔹 Difference Between List and ndarray

FeatureListndarray
Mixed typesYesNo
Memory efficientNoYes
Fast mathNoYes
VectorizedNoYes

🔹 Array vs Matrix

NumPy recommends:

Use ndarray not np.matrix.

Because:

  • matrix is older

  • Less flexible

  • Being phased out


🔹 Converting List to NumPy Array

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

🔹 Converting ndarray to List

arr.tolist()

🎯 When to Use NumPy?

Use NumPy when:

  • Handling large datasets

  • Performing math operations

  • Doing ML

  • Working with matrices

  • Image processing

  • Signal processing


🧠 Concept Check (Important)

If someone asks:

Why is NumPy fast?

You should answer:

  1. Homogeneous data

  2. Contiguous memory

  3. C implementation

  4. Vectorization

  5. No Python-level loops


📌 Summary of Module 1

You learned:

✔ What NumPy is
✔ Why NumPy is faster
✔ How to install
✔ How to import
✔ What ndarray is
✔ ndarray properties

Perfect 👌
Let’s build 📌 Module 2: Creating Arrays in NumPy in a structured, beginner → intermediate format with explanations + examples + practice.


📌 Module 2: Creating Arrays in NumPy

NumPy provides multiple ways to create arrays depending on your use case:

  • From Python lists

  • From ranges

  • From random numbers

  • From fixed values (zeros, ones)

  • Identity matrices

  • Evenly spaced values


1️⃣ np.array() – Create Array from Python List

This is the most basic way to create a NumPy array.


🔹 1D Array

import numpy as np

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

Output:

[1 2 3 4]

Check properties:

print(arr.shape)   # (4,)
print(arr.ndim)    # 1

🔹 2D Array

matrix = np.array([[1, 2], [3, 4]])
print(matrix)

Output:

[[1 2]
 [3 4]]

🔹 3D Array

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

print(arr3d)

Shape:

print(arr3d.shape)  # (2,2,2)

Meaning:

  • 2 blocks

  • Each block 2x2 matrix


2️⃣ np.zeros() – Array Filled with Zeros

Used when initializing empty structures.

zeros = np.zeros((2,3))
print(zeros)

Output:

[[0. 0. 0.]
 [0. 0. 0.]]

By default → float type.

Specify dtype:

np.zeros((2,3), dtype=int)

3️⃣ np.ones() – Array Filled with Ones

ones = np.ones((3,3))
print(ones)

4️⃣ np.eye() – Identity Matrix

Creates square matrix with 1 on diagonal.

identity = np.eye(3)
print(identity)

Output:

[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Useful in:

  • Linear algebra

  • Matrix operations

  • ML algorithms


5️⃣ np.arange() – Like Python range()

Creates evenly spaced values.

Syntax:

np.arange(start, stop, step)
arr = np.arange(0, 10, 2)
print(arr)

Output:

[0 2 4 6 8]

If step not given → default 1.


6️⃣ np.linspace() – Evenly Spaced Numbers (Important)

Creates fixed number of values between start and stop.

Syntax:

np.linspace(start, stop, number_of_values)
arr = np.linspace(0, 1, 5)
print(arr)

Output:

[0.   0.25 0.5  0.75 1.  ]

Difference from arange:

  • arange() → based on step

  • linspace() → based on number of values


7️⃣ np.random.rand() – Random Numbers (Uniform 0–1)

rand_arr = np.random.rand(2,3)
print(rand_arr)

Output:

[[0.23 0.78 0.12]
 [0.44 0.90 0.56]]

Generates:

  • Random floats between 0 and 1

  • Uniform distribution


8️⃣ np.random.randint() – Random Integers

rand_int = np.random.randint(1, 10, 5)
print(rand_int)

Generates:

  • 5 integers

  • Between 1 and 9

2D example:

np.random.randint(0, 100, (3,3))

🎯 Practical Examples


✅ Practice 1: Create Identity Matrix

Create 5x5 identity matrix:

identity = np.eye(5)
print(identity)

✅ Practice 2: Generate Random Dataset

Example: Create dataset of 100 samples with 3 features.

dataset = np.random.rand(100, 3)
print(dataset.shape)

Output:

(100, 3)

✅ Practice 3: Simulate Exam Scores (0–100)

scores = np.random.randint(0, 101, (10, 5))
print(scores)

Meaning:

  • 10 students

  • 5 subjects


✅ Practice 4: Create Time Series Data

time = np.linspace(0, 10, 100)
signal = np.sin(time)

print(signal[:5])

🔥 Important Comparison

FunctionUse Case
np.array()Convert list to array
np.zeros()Initialize empty structure
np.ones()Default filled array
np.eye()Identity matrix
np.arange()Sequence with step
np.linspace()Fixed count of values
np.random.rand()Random floats
np.random.randint()Random integers

🧠 Interview Question

Q: Difference between arange() and linspace()?

Answer:

  • arange() depends on step size.

  • linspace() depends on number of values.


📌 Module 2 Summary

You now know how to:

✔ Create 1D, 2D, 3D arrays
✔ Initialize zeros and ones
✔ Create identity matrices
✔ Generate sequences
✔ Generate random datasets


Perfect 👌
Let’s go deep dive mode on:

📌 Module 3: Array Attributes & Data Types (NumPy Deep Dive)

This module is very important for:

  • Performance optimization

  • Memory efficiency

  • Interview questions

  • Machine Learning fundamentals


🧠 1️⃣ Understanding NumPy Array Internally

A NumPy array (ndarray) has two major parts:

  1. Data buffer → Actual stored values

  2. Metadata → Information about array

Metadata includes:

  • Shape

  • Number of dimensions

  • Data type (dtype)

  • Size

  • Strides

  • Memory usage


🔹 2️⃣ Core Array Attributes

Let’s create an example:

import numpy as np

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

✅ 1. shape

Returns dimensions.

print(arr.shape)

Output:

(2, 3)

Meaning:

  • 2 rows

  • 3 columns

For 3D:

arr3d = np.zeros((2,3,4))
print(arr3d.shape)

Output:

(2,3,4)

✅ 2. ndim

Number of dimensions.

print(arr.ndim)

Output:

2

Examples:

  • 1D → ndim = 1

  • 2D → ndim = 2

  • 3D → ndim = 3


✅ 3. size

Total number of elements.

print(arr.size)

Output:

6

Formula:

size = rows × columns

✅ 4. dtype (Very Important)

Shows data type.

print(arr.dtype)

Output:

int64

NumPy arrays are:

Homogeneous → All elements same type


✅ 5. itemsize

Memory used by one element (in bytes).

print(arr.itemsize)

For int64:

8 bytes

Because:
64 bits = 8 bytes


✅ 6. nbytes

Total memory used.

print(arr.nbytes)

Output:

48

Why?

6 elements × 8 bytes = 48 bytes

✅ 7. strides (Advanced Concept)

Strides tell:

How many bytes to move in memory to go to next element

print(arr.strides)

Example output:

(24, 8)

Meaning:

  • Move 24 bytes to go to next row

  • Move 8 bytes to go to next column

This is why NumPy slicing is fast (it changes strides, not data).


 Data Types (dtype) Deep Dive

NumPy supports multiple types.


🔹 Integer Types

TypeBytes
int81
int162
int324
int648

Example:

arr = np.array([1,2,3], dtype=np.int8)
print(arr.dtype)

🔹 Float Types

TypePrecision
float16Low
float32Medium
float64High (default)

Example:

arr = np.array([1.2, 3.4], dtype=np.float32)
print(arr.dtype)

🔹 Boolean

arr = np.array([True, False, True])
print(arr.dtype)

🔹 Complex Numbers

arr = np.array([1+2j, 3+4j])
print(arr.dtype)

🧠 4️⃣ Why dtype Matters (Performance)

Example:

arr1 = np.ones((1000,1000), dtype=np.int64)
arr2 = np.ones((1000,1000), dtype=np.int8)

print(arr1.nbytes)
print(arr2.nbytes)

Memory difference:

  • int64 → 8MB

  • int8 → 1MB

Huge impact in ML systems.


🧠 5️⃣ Type Conversion (Casting)


🔹 Automatic Type Promotion

arr = np.array([1, 2.5])
print(arr.dtype)

Output:

float64

NumPy promotes to most general type.


🔹 Explicit Casting

arr = np.array([1,2,3])
new_arr = arr.astype(np.float32)

print(new_arr.dtype)

🧠 6️⃣ Checking Array Memory Layout


C Order (Row Major) – Default

Stored row by row.

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

Memory:

1 2 3 4

Fortran Order (Column Major)

arr = np.array([[1,2],[3,4]], order='F')

Memory:

1 3 2 4

Check:

print(arr.flags)

🧠 7️⃣ View vs Copy (Very Important)


🔹 View

arr = np.array([1,2,3])
b = arr.view()
b[0] = 100

print(arr)

Original changes.


🔹 Copy

b = arr.copy()
b[0] = 200

print(arr)

Original does NOT change.


🧠 8️⃣ Reshaping and Memory

Reshape doesn’t copy data if possible.

arr = np.arange(6)
reshaped = arr.reshape(2,3)

Efficient because:

  • Only metadata changes

  • Data buffer stays same


🧠 9️⃣ Real-World Example

Suppose you load large dataset:

data = np.random.rand(1_000_000)

Check memory:

print(data.nbytes / (1024*1024), "MB")

If memory issue:
Convert:

data = data.astype(np.float32)

Reduces memory by half.


🔥 Interview-Level Questions

Q: Why is NumPy memory efficient?

Answer:

  • Homogeneous dtype

  • Contiguous memory

  • No Python object overhead


Q: Difference between shape and size?

  • shape → dimensions

  • size → total elements


Q: Why is float64 default?

Because:

  • Higher precision

  • Suitable for scientific computing


📌 Summary

In this module you mastered:

✔ shape
✔ ndim
✔ size
✔ dtype
✔ itemsize
✔ nbytes
✔ strides
✔ Type casting
✔ Memory layout
✔ View vs Copy




🧠 1️⃣ 1D Indexing

Create 1D array:

import numpy as np

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

🔹 Access Single Element

print(arr[0])   # 10
print(arr[2])   # 30

Index starts at 0.


🧠 2️⃣ Negative Indexing

Negative index counts from end.

print(arr[-1])  # 50
print(arr[-2])  # 40

Useful when:

  • You want last element

  • Dynamic arrays


🧠 3️⃣ Slicing (1D)

Syntax:

array[start:stop]

Example:

print(arr[1:4])  # [20 30 40]

Important:

  • stop index NOT included


🔹 From Beginning

print(arr[:3])  # [10 20 30]

🔹 Till End

print(arr[2:])  # [30 40 50]

🧠 4️⃣ Step Slicing

Syntax:

array[start:stop:step]

🔹 Skip Values

print(arr[::2])   # [10 30 50]

🔹 Reverse Array

print(arr[::-1])  # [50 40 30 20 10]

🧠 5️⃣ 2D Indexing (Matrix)

Create matrix:

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

🔹 Access Single Element

print(matrix[0, 1])  # 2

Meaning:

  • Row 0

  • Column 1


🔹 Access Full Row

print(matrix[1])     # [4 5 6]

or

print(matrix[1, :])

🔹 Access Full Column

print(matrix[:, 1])  # [2 5 8]

Meaning:

  • All rows

  • Column 1


🧠 6️⃣ 2D Slicing


🔹 Extract Submatrix

print(matrix[0:2, 1:3])

Output:

[[2 3]
 [5 6]]

Meaning:

  • Rows 0–1

  • Columns 1–2


🧠 7️⃣ Boolean Indexing (Very Important)

Used for filtering data.


🔹 Example 1: Filter > 25

arr = np.array([10, 20, 30, 40, 50])
print(arr[arr > 25])

Output:

[30 40 50]

🔹 Example 2: Filter Even Numbers

print(arr[arr % 2 == 0])

🔹 2D Boolean Filtering

matrix = np.array([
    [5, 12, 7],
    [20, 3, 15]
])

print(matrix[matrix > 10])

Output:

[12 20 15]

Important:
Boolean indexing returns flattened result.


🧠 8️⃣ Fancy Indexing (Advanced)

Fancy indexing means:
Using arrays as indices.


🔹 1D Fancy Indexing

arr = np.array([10, 20, 30, 40, 50])
indices = [0, 2, 4]

print(arr[indices])

Output:

[10 30 50]

🔹 2D Fancy Indexing

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

rows = [0, 2]
cols = [1, 2]

print(matrix[rows, cols])

Output:

[2 9]

Explanation:

  • (0,1) → 2

  • (2,2) → 9


🧠 Fancy Indexing vs Slicing (Important Difference)

Slicing → Returns view (no copy)

Fancy indexing → Returns copy

Example:

arr = np.array([1,2,3,4])
slice_arr = arr[1:3]
slice_arr[0] = 100

print(arr)  # Changed

But:

arr = np.array([1,2,3,4])
fancy = arr[[1,2]]
fancy[0] = 100

print(arr)  # Not changed

🎯 Practice Section


✅ Practice 1: Extract Rows

matrix = np.arange(1,13).reshape(3,4)
print(matrix)

Extract 2nd row:

print(matrix[1])

✅ Practice 2: Extract Columns

Extract 3rd column:

print(matrix[:,2])

✅ Practice 3: Extract Submatrix

Extract first 2 rows and first 2 columns:

print(matrix[:2, :2])

✅ Practice 4: Filter Values > 10

matrix = np.array([
    [5, 12, 7],
    [20, 3, 15]
])

print(matrix[matrix > 10])

✅ Practice 5: Replace Values > 10

matrix[matrix > 10] = 999
print(matrix)

🧠 Real-World Example (ML Style)

Filter high salaries:

salaries = np.array([25000, 50000, 75000, 30000])

high_income = salaries[salaries > 40000]
print(high_income)

🔥 Interview Questions

Q: Difference between slicing and fancy indexing?

  • Slicing → View

  • Fancy indexing → Copy


Q: How to extract column from matrix?

matrix[:, column_index]

Q: How to reverse array?

arr[::-1]

📌 Module 4 Summary

You mastered:

✔ 1D indexing
✔ 2D indexing
✔ Negative indexing
✔ Slicing
✔ Step slicing
✔ Boolean indexing
✔ Fancy indexing
✔ View vs Copy difference


📌 Module 5: Array Operations & Vectorization (Deep Dive)

This module explains:

  • How NumPy performs operations

  • Why vectorization is powerful

  • Why you should avoid Python loops

  • How NumPy makes ML fast


🧠 1️⃣ What is Vectorization?

Vectorization means:

Performing operations on entire arrays at once
WITHOUT writing explicit Python loops

Instead of:

result = []
for x in arr:
    result.append(x * 2)

We do:

result = arr * 2

Cleaner ✅
Faster ✅
More memory efficient ✅


🧠 2️⃣ Element-wise Operations

Create arrays:

import numpy as np

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

🔹 Addition

print(a + b)

Output:

[5 7 9]

🔹 Subtraction

print(a - b)

🔹 Multiplication

print(a * b)

🔹 Division

print(a / b)

Important:

Operations are element-wise, NOT matrix multiplication.


🧠 3️⃣ Scalar Operations

NumPy automatically applies scalar to every element.

print(a + 10)
print(a * 3)
print(a ** 2)

This is broadcasting in action (explained deeper later).


🧠 4️⃣ Comparison Operations

print(a > 2)

Output:

[False False  True]

Use with Filtering

print(a[a > 1])

Output:

[2 3]

🧠 5️⃣ Logical Operations

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

print((a > 1) & (a < 4))

Output:

[False  True  True False]

⚠ Important:
Use & not and
Use | not or


🧠 6️⃣ Mathematical Functions (Vectorized)

NumPy functions work on entire arrays.

arr = np.array([1, 4, 9])

print(np.sqrt(arr))
print(np.log(arr))
print(np.exp(arr))
print(np.sin(arr))

No loops needed.


🧠 7️⃣ Aggregation Operations

Aggregation reduces array to single value.


🔹 Sum

arr = np.array([1, 2, 3, 4])
print(np.sum(arr))

🔹 Mean

print(np.mean(arr))

🔹 Min / Max

print(np.min(arr))
print(np.max(arr))

🔹 Axis Parameter (Very Important)

2D example:

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

Column-wise sum

print(np.sum(matrix, axis=0))

Output:

[5 7 9]

Row-wise sum

print(np.sum(matrix, axis=1))

Output:

[6 15]

🧠 8️⃣ Matrix Multiplication vs Element-wise


🔹 Element-wise

a * b

🔹 Dot Product

np.dot(a, b)

🔹 Matrix Multiplication

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

print(A @ B)

OR

np.matmul(A, B)

🧠 9️⃣ Why Vectorization is Faster

Let’s compare speed:

import time

size = 1_000_000
arr = np.arange(size)

# Python loop
start = time.time()
result = []
for x in arr:
    result.append(x * 2)
print("Loop time:", time.time() - start)

# NumPy
start = time.time()
result = arr * 2
print("Vectorized time:", time.time() - start)

NumPy wins because:

  • Computation done in C

  • No Python loop overhead

  • CPU optimized


🧠 1️⃣0️⃣ Broadcasting (Intro)

Example:

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

print(a + b)

Output:

[[2 3 4]
 [5 6 7]]

NumPy automatically expands smaller array.

(Broadcasting will be next module deep dive.)


🧠 1️⃣1️⃣ In-place Operations (Performance)

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

arr += 10
print(arr)

Better for memory efficiency.


🧠 1️⃣2️⃣ Practical ML Example

Normalize dataset:

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

mean = np.mean(data)
std = np.std(data)

normalized = (data - mean) / std
print(normalized)

Fully vectorized.


🎯 Practice Problems


✅ 1. Multiply all values by 5

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

✅ 2. Find all values greater than 10

arr = np.array([5,12,7,20,15])
print(arr[arr > 10])

✅ 3. Compute row-wise mean

matrix = np.array([[1,2,3],[4,5,6]])
print(np.mean(matrix, axis=1))

✅ 4. Square each element

arr = np.array([2,3,4])
print(arr ** 2)

🔥 Interview Questions

Q: Why is NumPy faster than Python loops?

Answer:

  • Vectorization

  • C implementation

  • Contiguous memory

  • No Python-level iteration


Q: Difference between * and @?

  • * → element-wise

  • @ → matrix multiplication


Q: What is axis in NumPy?

  • axis=0 → column operation

  • axis=1 → row operation


📌 Module 5 Summary

You mastered:

✔ Element-wise operations
✔ Scalar operations
✔ Comparison & logical operations
✔ Aggregation functions
✔ Axis concept
✔ Matrix multiplication
✔ Vectorization benefits
✔ In-place operations



📌 Module 6: Broadcasting (Deep Dive – VERY IMPORTANT)

Broadcasting allows NumPy to perform operations on arrays of different shapes.


🧠 1️⃣ Why Broadcasting Exists

Instead of manually repeating arrays:

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

b = np.array([1,1,1])

NumPy automatically expands b:

print(a + b)

Output:

[[2 3 4]
 [5 6 7]]

No loops. No manual expansion.


🧠 2️⃣ Broadcasting Rules (Must Memorize)

When operating on two arrays:

  1. Compare shapes from right to left

  2. Two dimensions are compatible if:

    • They are equal

    • OR one of them is 1


Example 1

(2,3)
(3,)

Compare:

(2,3)
(1,3)

Compatible ✅


Example 2

(2,3)
(2,1)

Result shape → (2,3)


Example 3 (Error)

(2,3)
(2,2)

❌ Not compatible


🧠 3️⃣ Broadcasting Internally

Important:

Broadcasting does NOT copy data
It creates a virtual expansion using strides.

Memory efficient ✅


🧠 4️⃣ Practical ML Example

Normalize dataset column-wise:

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

mean = np.mean(data, axis=0)
std = np.std(data, axis=0)

normalized = (data - mean) / std

This works because of broadcasting.


📌 Module 7: Reshaping & Array Manipulation


🧠 1️⃣ reshape()

arr = np.arange(6)
reshaped = arr.reshape(2,3)

Important:
Total elements must match.


🧠 2️⃣ flatten() vs ravel()

flatten()

  • Returns copy

ravel()

  • Returns view (if possible)


🧠 3️⃣ transpose()

matrix = np.array([[1,2],[3,4]])
print(matrix.T)

Swaps rows and columns.


🧠 4️⃣ expand_dims()

arr = np.array([1,2,3])
print(np.expand_dims(arr, axis=0))

Shape changes from (3,) → (1,3)


🧠 5️⃣ squeeze()

Removes dimensions of size 1.

arr = np.array([[1,2,3]])
print(np.squeeze(arr))

🧠 6️⃣ concatenate()

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

print(np.concatenate((a,b), axis=0))

📌 Module 8: Linear Algebra

NumPy has powerful np.linalg.


🧠 1️⃣ Dot Product

a = np.array([1,2])
b = np.array([3,4])
print(np.dot(a,b))

🧠 2️⃣ Matrix Multiplication

A @ B

🧠 3️⃣ Determinant

np.linalg.det(A)

🧠 4️⃣ Inverse

np.linalg.inv(A)

🧠 5️⃣ Eigenvalues

np.linalg.eig(A)

Used in:

  • PCA

  • ML

  • Signal processing


🧠 6️⃣ Norm

np.linalg.norm(A)

📌 Module 9: NumPy Performance Optimization

This separates beginner from advanced.


🔥 1️⃣ Avoid Python Loops

Bad:

for i in range(len(arr)):
    arr[i] *= 2

Good:

arr *= 2

🔥 2️⃣ Use Vectorization

Use built-in functions:

  • np.sum

  • np.mean

  • np.sqrt

  • np.dot


🔥 3️⃣ Choose Correct dtype

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

Reduces memory usage.


🔥 4️⃣ In-place Operations

arr += 10

Avoids new memory allocation.


🔥 5️⃣ Avoid Unnecessary Copies

Use slicing instead of fancy indexing when possible.


🔥 6️⃣ Use Broadcasting Instead of Tiling

Bad:

np.tile(b, (2,1))

Good:
Let broadcasting handle it.


📌 Module 10: NumPy Interview Questions (Deep)


Q1: Why is NumPy faster than Python list?

Answer:

  • Homogeneous data

  • Contiguous memory

  • C implementation

  • Vectorization

  • No Python loops


Q2: Difference between reshape and resize?

  • reshape → returns new view (if possible)

  • resize → modifies original array


Q3: Difference between flatten and ravel?

  • flatten → copy

  • ravel → view (if possible)


Q4: Difference between dot and matmul?

  • dot → vector dot + matrix multiplication

  • matmul (@) → strict matrix multiplication


Q5: What is broadcasting?

Explain rules clearly.


Q6: What is stride?

Number of bytes to move in memory to next element.


Q7: How to check memory used by array?

arr.nbytes

Q8: Why is float64 default?

Precision for scientific computing.


🏆 After Completing This

You should be able to:

✔ Build ML algorithms
✔ Handle multi-dimensional tensors
✔ Optimize performance
✔ Answer NumPy interview questions
✔ Debug broadcasting errors


🚀 If You Want Next

We can now go into:

  • 🔥 NumPy for Machine Learning (implement Linear Regression)

  • 🔥 NumPy vs Pandas vs PyTorch

  • 🔥 50 Advanced NumPy Practice Problems

  • 🔥 Build Mini ML Project from scratch

  • 🔥 Memory Internals Deep Dive

Tell me your next level 😎


📌 Module 6: Broadcasting (Very Important – Deep Dive)

Broadcasting is the reason NumPy code looks clean and runs fast.

If you understand broadcasting properly, you unlock:

  • Cleaner code

  • Faster ML pipelines

  • No unnecessary memory duplication


🧠 1️⃣ What is Broadcasting?

Broadcasting allows NumPy to:

Perform operations on arrays of different shapes
WITHOUT explicitly copying data

Example:

import numpy as np

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

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

print(a + b)

Output:

[[11 22 33]
 [14 25 36]]

How did this work?

NumPy automatically expanded b to match shape of a.


🧠 2️⃣ Broadcasting Rules (Must Memorize)

When operating on two arrays:

Step 1: Compare shapes from RIGHT to LEFT

Example:

(2,3)
(3,)

Compare rightmost dimension first.


Step 2: Dimensions are compatible if:

✔ They are equal
OR
✔ One of them is 1

If neither → ❌ Error


🧠 3️⃣ Shape Compatibility Examples


✅ Example 1

(2,3)
(3,)

Rewrite as:

(2,3)
(1,3)

Compatible ✅

Result shape → (2,3)


✅ Example 2

(4,3)
(4,1)

Compare:

  • 3 vs 1 → OK

  • 4 vs 4 → OK

Result shape → (4,3)


❌ Example 3 (Error)

(2,3)
(2,2)

Compare:

  • 3 vs 2 → ❌ Not equal and not 1

Error:

ValueError: operands could not be broadcast together

🧠 4️⃣ Examples with 1D + 2D


🔹 Case 1: Add Row Vector to Matrix

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

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

print(a + b)

Here:

a → (2,3)
b → (3,)

b treated as:

(1,3)

Then expanded to:

(2,3)

🔹 Case 2: Add Column Vector to Matrix

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

b = np.array([[10],
              [20]])

print(a + b)

Shapes:

a → (2,3)
b → (2,1)

Result:

[[11 12 13]
 [24 25 26]]

Here b expanded column-wise.


🔹 Case 3: Force Broadcasting Using reshape()

Sometimes 1D doesn't behave as expected.

Example:

b = np.array([10,20])

To make it column vector:

b = b.reshape(2,1)

or

b = b[:, np.newaxis]

🧠 5️⃣ Internals: No Real Copy Happens

Important:

Broadcasting does NOT duplicate data.

NumPy uses:

  • Strides

  • Virtual expansion

Memory efficient ✅


🧠 6️⃣ Common Broadcasting Errors


❌ Error 1: Shape Mismatch

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

b = np.array([10,20])

print(a + b)

Shapes:

(2,3)
(2,)

Compare right to left:

3 vs 2 ❌

Fix:

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

OR reshape properly.


❌ Error 2: Forgetting axis

Example:

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

mean = np.mean(data)

mean becomes scalar → wrong normalization.

Correct:

mean = np.mean(data, axis=0)

❌ Error 3: 1D Ambiguity

1D arrays can be:

  • Row vector?

  • Column vector?

NumPy treats 1D as neither.

Always reshape if necessary.


🧠 7️⃣ Real-World Normalization Example (Very Important)

Suppose dataset:

data = np.array([
    [10, 200, 3000],
    [20, 400, 6000],
    [30, 600, 9000]
])

Goal:
Normalize each column.


Step 1: Compute mean and std

mean = np.mean(data, axis=0)
std = np.std(data, axis=0)

Shapes:

data → (3,3)
mean → (3,)
std  → (3,)

Step 2: Normalize

normalized = (data - mean) / std

Broadcasting happens automatically.

No loops needed.


🧠 8️⃣ Advanced Broadcasting Example

Create grid:

x = np.arange(5)
y = np.arange(3)

print(x)
print(y)

To create coordinate grid:

print(x[np.newaxis, :] + y[:, np.newaxis])

Result:

[[0 1 2 3 4]
 [1 2 3 4 5]
 [2 3 4 5 6]]

Used in:

  • Image processing

  • Mesh grids

  • Deep learning


🧠 9️⃣ Broadcasting vs np.tile()

Avoid:

np.tile(b, (2,1))

Use broadcasting instead:

a + b

Broadcasting is:

  • Faster

  • Memory efficient

  • Cleaner


🔥 Interview Questions


Q1: What is broadcasting?

Ability of NumPy to perform arithmetic on different shaped arrays without copying data.


Q2: What are broadcasting rules?

  • Compare shapes from right

  • Dimensions compatible if equal or 1


Q3: Why broadcasting is memory efficient?

Because no real duplication occurs — virtual expansion using strides.


Q4: How to fix broadcasting error?

  • Reshape array

  • Add new axis

  • Match dimensions


📌 Module 6 Summary

You mastered:

✔ Broadcasting rules
✔ Shape compatibility
✔ 1D + 2D operations
✔ Common broadcasting errors
✔ Column-wise normalization
✔ Memory efficiency
✔ Real-world ML example

 NumPy modules:

📌 Module 7: Reshaping & Manipulating Arrays (Deep Dive)

Reshaping and manipulating arrays is critical for:

  • Machine Learning

  • Deep Learning

  • Data preprocessing

  • Image processing

  • Broadcasting compatibility


🧠 1️⃣ reshape() – Change Shape Without Changing Data

reshape() changes the dimensions of an array.


🔹 Basic Example

import numpy as np

arr = np.arange(6)
print(arr)

Output:

[0 1 2 3 4 5]

Reshape into 2×3:

reshaped = arr.reshape(2, 3)
print(reshaped)

Output:

[[0 1 2]
 [3 4 5]]

🔹 Important Rule

Total elements must match.

6 elements → can reshape to:
(2,3)
(3,2)
(1,6)
(6,1)

But NOT:

(4,2) ❌

🔹 Automatic Dimension (-1)

Let NumPy calculate one dimension:

arr.reshape(2, -1)

NumPy computes automatically.


🔹 Memory Behavior

reshape():

  • Returns view if possible

  • Otherwise returns copy


🧠 2️⃣ flatten() – Returns a Copy

Converts multi-dimensional array into 1D.

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

flat = matrix.flatten()
print(flat)

Output:

[1 2 3 4]

Important:

✔ Always returns copy
✔ Changes to flattened array do NOT affect original


🧠 3️⃣ ravel() – Returns View (If Possible)

Similar to flatten, but memory-efficient.

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

r = matrix.ravel()
print(r)

Difference:

  • flatten() → copy

  • ravel() → view (if possible)


🔹 Proof

r[0] = 100
print(matrix)

Original changes if view exists.


🧠 4️⃣ transpose() – Swap Axes

Transpose flips dimensions.


🔹 2D Example

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

print(matrix.T)

Output:

[[1 4]
 [2 5]
 [3 6]]

🔹 Multi-Dimensional Example

arr = np.random.rand(2,3,4)
transposed = arr.transpose(1,0,2)

This reorders axes.


🧠 5️⃣ swapaxes()

Swaps two specific axes.


Example

arr = np.random.rand(2,3,4)

swapped = arr.swapaxes(0,1)
print(swapped.shape)

If original:

(2,3,4)

After swap:

(3,2,4)

Used in:

  • Deep learning tensor manipulation


🧠 6️⃣ expand_dims() – Add New Axis

Used for:

  • Converting 1D → 2D

  • Preparing ML input shapes


Example

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

expanded = np.expand_dims(arr, axis=0)
print(expanded.shape)

Output:

(1,3)

Column Vector

expanded = np.expand_dims(arr, axis=1)
print(expanded.shape)

Output:

(3,1)

Equivalent:

arr[:, np.newaxis]

🧠 7️⃣ squeeze() – Remove Size-1 Dimensions

Removes unnecessary dimensions.


Example

arr = np.array([[[1,2,3]]])
print(arr.shape)

Output:

(1,1,3)

Use squeeze:

s = np.squeeze(arr)
print(s.shape)

Output:

(3,)

🧠 Practical ML Examples


Example 1: Preparing Data for Model

Suppose model expects:

(samples, features)

If you have:

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

Shape:

(3,)

Fix:

x = x.reshape(1, -1)

Now:

(1,3)

Example 2: Image Processing

Images stored as:

(height, width, channels)

Sometimes grayscale image shape:

(28,28)

Add channel dimension:

image = image[:, :, np.newaxis]

New shape:

(28,28,1)

🧠 flatten() vs ravel() Summary

Featureflatten()ravel()
CopyYesUsually No
Memory efficientNoYes
SafeYesMight affect original

🧠 reshape() vs transpose()

reshapetranspose
Changes shapeSwaps axes
Data order preservedData orientation changed

🧠 Common Errors


❌ Wrong reshape size

arr.reshape(4,4)

Error if total elements mismatch.


❌ Confusing 1D vs 2D

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

No change! Because 1D has no row/column.

Must reshape first.


🔥 Interview Questions


Q1: Difference between flatten() and ravel()?

  • flatten → copy

  • ravel → view (if possible)


Q2: Why reshape sometimes returns copy?

If memory layout not compatible.


Q3: When to use expand_dims?

To match broadcasting or ML input shape.


Q4: How to convert row vector to column vector?

arr.reshape(-1,1)

📌 Module 7 Summary

You mastered:

✔ reshape()
✔ flatten()
✔ ravel()
✔ transpose()
✔ swapaxes()
✔ expand_dims()
✔ squeeze()
✔ Real ML shape handling

📌 Module 8: NumPy Stacking & Splitting (Detailed Guide)

This module is very important for ML, Data Processing, and Feature Engineering — because real-world data often needs to be combined (stacked) or divided (split).

We’ll cover:

  • vstack()

  • hstack()

  • concatenate()

  • split()

  • hsplit()

  • vsplit()


🔷 1️⃣ vstack() – Vertical Stack

👉 Stacks arrays row-wise (one below another)

🔹 Syntax:

np.vstack((arr1, arr2))

🔹 Example:

import numpy as np

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

result = np.vstack((a, b))
print(result)

🔹 Output:

[[1 2 3]
 [4 5 6]]

✔ Adds arrays vertically
✔ Result becomes 2D


🔹 2D Example:

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

print(np.vstack((a,b)))

🔷 2️⃣ hstack() – Horizontal Stack

👉 Stacks arrays column-wise (side by side)

🔹 Syntax:

np.hstack((arr1, arr2))

🔹 Example:

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

result = np.hstack((a, b))
print(result)

🔹 Output:

[1 2 3 4 5 6]

✔ Adds arrays horizontally
✔ Keeps it 1D if inputs are 1D


🔹 2D Example:

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

print(np.hstack((a,b)))

Output:

[[1 4]
 [2 5]
 [3 6]]

🔷 3️⃣ concatenate() – Flexible Join

👉 Most powerful stacking function
👉 You control the axis

🔹 Syntax:

np.concatenate((arr1, arr2), axis=0 or 1)

🔹 Example 1: Vertical (axis=0)

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

print(np.concatenate((a,b), axis=0))

Output:

[[1 2]
 [3 4]
 [5 6]
 [7 8]]

🔹 Example 2: Horizontal (axis=1)

print(np.concatenate((a,b), axis=1))

Output:

[[1 2 5 6]
 [3 4 7 8]]

⚠ Important Rule:

Shapes must match except along the joining axis.


🔷 4️⃣ split() – Split Array

👉 Splits array into equal parts

🔹 Syntax:

np.split(array, sections, axis=0)

🔹 Example:

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

print(np.split(a, 3))

Output:

[array([1,2]), array([3,4]), array([5,6])]

🔹 2D Example:

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

print(np.split(a, 2, axis=0))

Splits into 2 vertical parts.


🔷 5️⃣ hsplit() – Horizontal Split

👉 Splits array column-wise

🔹 Example:

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

print(np.hsplit(a, 2))

Output:

[array([[1,2],
        [5,6]]),

 array([[3,4],
        [7,8]])]

✔ Splits columns
✔ Requires equal division


🔷 6️⃣ vsplit() – Vertical Split

👉 Splits array row-wise

🔹 Example:

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

print(np.vsplit(a, 2))

Output:

[array([[1,2],
        [3,4]]),

 array([[5,6],
        [7,8]])]

✔ Splits rows
✔ Requires equal division


🎯 Difference Summary

FunctionDirectionWorks On
vstack()Add rowsStack vertically
hstack()Add columnsStack horizontally
concatenate()Customaxis=0 or 1
split()Split genericaxis control
hsplit()Split columnsHorizontal
vsplit()Split rowsVertical

🚀 Interview + ML Insight

In ML pipelines:

  • hstack() → Combine features

  • vstack() → Combine datasets

  • concatenate() → Flexible merging

  • split() → Train/test splitting

  • hsplit() → Feature separation

  • vsplit() → Batch splitting


📌 Module 9: Mathematical Functions in NumPy

Since you're building strong foundations for ML / Data Science, understanding NumPy mathematical functions deeply is very important. These are heavily used in ML algorithms, statistics, feature engineering, and signal processing.


🔹 1️⃣ Square Root – np.sqrt()

Computes the square root element-wise.

✅ Syntax:

np.sqrt(array)

✅ Example:

import numpy as np

arr = np.array([1, 4, 9, 16])
print(np.sqrt(arr))

Output:

[1. 2. 3. 4.]

📌 Use Case:

  • Distance calculation (Euclidean distance)

  • Standard deviation formula


🔹 2️⃣ Exponential – np.exp()

Computes e^x (Euler’s number ≈ 2.718).

✅ Syntax:

np.exp(array)

✅ Example:

arr = np.array([1, 2, 3])
print(np.exp(arr))

📌 Use Case:

  • Logistic regression (sigmoid function)

  • Neural networks

  • Growth models


🔹 3️⃣ Logarithm – np.log()

Computes natural log (base e).

✅ Syntax:

np.log(array)

Example:

arr = np.array([1, np.e, 10])
print(np.log(arr))

Other log variants:

np.log10(arr)   # Base 10
np.log2(arr)    # Base 2

📌 Use Case:

  • Log loss

  • Data normalization

  • Financial calculations

⚠️ Log of negative numbers → NaN
⚠️ Log of 0 → -inf


🔹 4️⃣ Power – np.power()

Raises elements to a specified power.

✅ Syntax:

np.power(base, exponent)

Example:

arr = np.array([2, 3, 4])
print(np.power(arr, 2))

Output:

[ 4  9 16]

Same as:

arr ** 2

🔹 5️⃣ Absolute – np.abs()

Returns absolute value.

Example:

arr = np.array([-5, -1, 3])
print(np.abs(arr))

Output:

[5 1 3]

📌 Use Case:

  • Error calculation (MAE)

  • Distance formulas


🔹 6️⃣ Trigonometric Functions (Very Important)

NumPy provides element-wise trigonometric functions.

⚠️ Important: NumPy uses radians, NOT degrees.


📍 Basic Trigonometric Functions

1️⃣ np.sin()

2️⃣ np.cos()

3️⃣ np.tan()

angles = np.array([0, np.pi/2, np.pi])
print(np.sin(angles))
print(np.cos(angles))

📍 Convert Degrees to Radians

np.deg2rad(90)

Example:

degrees = np.array([0, 90, 180])
radians = np.deg2rad(degrees)
print(np.sin(radians))

📍 Inverse Trigonometric Functions

1️⃣ np.arcsin()

2️⃣ np.arccos()

3️⃣ np.arctan()

values = np.array([0, 1])
print(np.arcsin(values))

📍 Hyperbolic Functions

1️⃣ np.sinh()

2️⃣ np.cosh()

3️⃣ np.tanh()

arr = np.array([0, 1])
print(np.tanh(arr))

📌 Used in:

  • Neural networks (tanh activation)


🔥 Summary Table

FunctionDescription
np.sqrt()Square root
np.exp()e^x
np.log()Natural log
np.power()Raise to power
np.abs()Absolute value
np.sin()Sine
np.cos()Cosine
np.tan()Tangent


📌 Module 7: Reshaping & Manipulating Arrays (Deep Dive)

Reshaping and manipulating arrays is critical for:

  • Machine Learning

  • Deep Learning

  • Data preprocessing

  • Image processing

  • Broadcasting compatibility


🧠 1️⃣ reshape() – Change Shape Without Changing Data

reshape() changes the dimensions of an array.


🔹 Basic Example

import numpy as np

arr = np.arange(6)
print(arr)

Output:

[0 1 2 3 4 5]

Reshape into 2×3:

reshaped = arr.reshape(2, 3)
print(reshaped)

Output:

[[0 1 2]
 [3 4 5]]

🔹 Important Rule

Total elements must match.

6 elements → can reshape to:
(2,3)
(3,2)
(1,6)
(6,1)

But NOT:

(4,2) ❌

🔹 Automatic Dimension (-1)

Let NumPy calculate one dimension:

arr.reshape(2, -1)

NumPy computes automatically.


🔹 Memory Behavior

reshape():

  • Returns view if possible

  • Otherwise returns copy


🧠 2️⃣ flatten() – Returns a Copy

Converts multi-dimensional array into 1D.

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

flat = matrix.flatten()
print(flat)

Output:

[1 2 3 4]

Important:

✔ Always returns copy
✔ Changes to flattened array do NOT affect original


🧠 3️⃣ ravel() – Returns View (If Possible)

Similar to flatten, but memory-efficient.

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

r = matrix.ravel()
print(r)

Difference:

  • flatten() → copy

  • ravel() → view (if possible)


🔹 Proof

r[0] = 100
print(matrix)

Original changes if view exists.


🧠 4️⃣ transpose() – Swap Axes

Transpose flips dimensions.


🔹 2D Example

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

print(matrix.T)

Output:

[[1 4]
 [2 5]
 [3 6]]

🔹 Multi-Dimensional Example

arr = np.random.rand(2,3,4)
transposed = arr.transpose(1,0,2)

This reorders axes.


🧠 5️⃣ swapaxes()

Swaps two specific axes.


Example

arr = np.random.rand(2,3,4)

swapped = arr.swapaxes(0,1)
print(swapped.shape)

If original:

(2,3,4)

After swap:

(3,2,4)

Used in:

  • Deep learning tensor manipulation


🧠 6️⃣ expand_dims() – Add New Axis

Used for:

  • Converting 1D → 2D

  • Preparing ML input shapes


Example

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

expanded = np.expand_dims(arr, axis=0)
print(expanded.shape)

Output:

(1,3)

Column Vector

expanded = np.expand_dims(arr, axis=1)
print(expanded.shape)

Output:

(3,1)

Equivalent:

arr[:, np.newaxis]

🧠 7️⃣ squeeze() – Remove Size-1 Dimensions

Removes unnecessary dimensions.


Example

arr = np.array([[[1,2,3]]])
print(arr.shape)

Output:

(1,1,3)

Use squeeze:

s = np.squeeze(arr)
print(s.shape)

Output:

(3,)

🧠 Practical ML Examples


Example 1: Preparing Data for Model

Suppose model expects:

(samples, features)

If you have:

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

Shape:

(3,)

Fix:

x = x.reshape(1, -1)

Now:

(1,3)

Example 2: Image Processing

Images stored as:

(height, width, channels)

Sometimes grayscale image shape:

(28,28)

Add channel dimension:

image = image[:, :, np.newaxis]

New shape:

(28,28,1)

🧠 flatten() vs ravel() Summary

Featureflatten()ravel()
CopyYesUsually No
Memory efficientNoYes
SafeYesMight affect original

🧠 reshape() vs transpose()

reshapetranspose
Changes shapeSwaps axes
Data order preservedData orientation changed

🧠 Common Errors


❌ Wrong reshape size

arr.reshape(4,4)

Error if total elements mismatch.


❌ Confusing 1D vs 2D

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

No change! Because 1D has no row/column.

Must reshape first.


🔥 Interview Questions


Q1: Difference between flatten() and ravel()?

  • flatten → copy

  • ravel → view (if possible)


Q2: Why reshape sometimes returns copy?

If memory layout not compatible.


Q3: When to use expand_dims?

To match broadcasting or ML input shape.


Q4: How to convert row vector to column vector?

arr.reshape(-1,1)

📌 Module 7 Summary

You mastered:

✔ reshape()
✔ flatten()
✔ ravel()
✔ transpose()
✔ swapaxes()
✔ expand_dims()
✔ squeeze()
✔ Real ML shape handling

  1. mean, median, std, var, min, max, argmin, argmax

  2. axis parameter (very important)

  3. Practice:

    • Column-wise mean

    • Normalize a dataset (step-by-step, with code)


1. Basic Statistical Functions in NumPy

First, import NumPy:

import numpy as np

Let’s use a sample array:

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

1.1 np.mean – Average value

What it does:
Adds all elements and divides by the number of elements.

np.mean(a)             # mean of all elements
np.mean(a, axis=0)     # mean column-wise
np.mean(a, axis=1)     # mean row-wise

For a:

  • np.mean(a) → (1+2+...+9) / 9 = 5.0

  • np.mean(a, axis=0) → [4., 5., 6.] (means of each column)

  • np.mean(a, axis=1) → [2., 5., 8.] (means of each row)


1.2 np.median – Middle value

What it does:
Sorts values and picks the middle one. If even count → average of two middle values.

np.median(a)           # median of all elements
np.median(a, axis=0)   # median column-wise
np.median(a, axis=1)   # median row-wise

For a:

  • np.median(a) → 5.0

  • np.median(a, axis=0) → [4., 5., 6.]

  • np.median(a, axis=1) → [2., 5., 8.]


1.3 np.std – Standard Deviation

What it does:
Measures how spread out the data is around the mean.

Formula (population std):

[
\sigma = \sqrt{\frac{\sum (x_i - \mu)^2}{N}}
]

np.std(a)              # std of all elements
np.std(a, axis=0)      # std column-wise
np.std(a, axis=1)      # std row-wise

You can also use ddof=1 for sample std:

np.std(a, ddof=1)

1.4 np.var – Variance

What it does:
Variance = (standard deviation)²

Formula:

[
\sigma^2 = \frac{\sum (x_i - \mu)^2}{N}
]

np.var(a)              # variance of all elements
np.var(a, axis=0)      # variance column-wise
np.var(a, axis=1)      # variance row-wise

1.5 np.min and np.max – Minimum & Maximum

np.min(a)              # smallest element in entire array
np.max(a)              # largest element in entire array

np.min(a, axis=0)      # column-wise min
np.max(a, axis=0)      # column-wise max

np.min(a, axis=1)      # row-wise min
np.max(a, axis=1)      # row-wise max

For a:

  • np.min(a) → 1

  • np.max(a) → 9

  • np.min(a, axis=0) → [1, 2, 3]

  • np.max(a, axis=0) → [7, 8, 9]


1.6 np.argmin and np.argmax – Indices of Min/Max

What they do:
Return the index of the minimum or maximum element.

Without axis, it gives flat index (as if the array is flattened into 1D):

np.argmin(a)     # index of smallest element in flattened array
np.argmax(a)     # index of largest element in flattened array

For a:

  • Flattened a is [1,2,3,4,5,6,7,8,9]

  • np.argmin(a) → 0 (value 1)

  • np.argmax(a) → 8 (value 9)

With axis, it gives indices along that axis:

np.argmin(a, axis=0)   # index of min in each column
np.argmax(a, axis=0)   # index of max in each column

np.argmin(a, axis=1)   # index of min in each row
np.argmax(a, axis=1)   # index of max in each row

Example:

np.argmin(a, axis=0)   # [0, 0, 0] (all mins in first row)
np.argmax(a, axis=0)   # [2, 2, 2] (all max in last row)

2. The axis Parameter (Very Important)

Think like this:

axis is the dimension you collapse (reduce).

For a 2D array shape = (rows, cols):

  • axis=0 → operate down the rows, result per column
    (so you get 1 value per column → shape (cols,))

  • axis=1 → operate across the columns, result per row
    (so you get 1 value per row → shape (rows,))

Example:

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

np.mean(a, axis=0)  # → [4., 5., 6.]  (column-wise)
np.mean(a, axis=1)  # → [2., 5., 8.]  (row-wise)

You can use axis with almost all reduction functions: sum, mean, min, max, std, var, argmax, argmin, etc.


3. Practice 1: Compute Column-wise Mean

Let’s say you have a dataset with rows = samples, columns = features:

data = np.array([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0],
    [7.0, 8.0, 9.0],
    [10.0, 11.0, 12.0]
])

Task: Column-wise mean

We want mean of each column → axis=0:

col_means = np.mean(data, axis=0)
print(col_means)

Output:

[5.5 6.5 7.5]

Explanation:

  • Column 1 mean: (1 + 4 + 7 + 10) / 4 = 22 / 4 = 5.5

  • Column 2 mean: (2 + 5 + 8 + 11) / 4 = 26 / 4 = 6.5

  • Column 3 mean: (3 + 6 + 9 + 12) / 4 = 30 / 4 = 7.5

So each feature’s average is captured.


4. Practice 2: Normalize a Dataset (Step-by-step)

There are many types of normalization. I’ll show two common ones:

  1. Z-score normalization (standardization) – uses mean and std

  2. Min-Max scaling – uses min and max

We’ll assume again:

  • Rows = samples

  • Columns = features

4.1 Z-score Normalization (Standardization)

Formula per feature:

[
x' = \frac{x - \mu}{\sigma}
]

Where:

  • ( \mu ) = mean of the feature (column)

  • ( \sigma ) = standard deviation of the feature (column)

Step-by-step with NumPy:

import numpy as np

# Example dataset: 4 samples, 3 features
data = np.array([
    [1.0,  200.0,  0.5],
    [2.0,  220.0,  0.7],
    [3.0,  240.0,  0.8],
    [4.0,  260.0,  0.9]
])

# 1️⃣ Compute column-wise mean and std
mean = np.mean(data, axis=0)
std = np.std(data, axis=0)

print("Mean :", mean)
print("Std  :", std)

# 2️⃣ Apply normalization: (x - mean) / std
data_normalized = (data - mean) / std

print("Normalized data:\n", data_normalized)

What’s happening:

  • data - mean → NumPy broadcasts mean across all rows

  • / std → then divides each column by its std (again using broadcasting)

  • Result: each column will have:

    • mean ≈ 0

    • std ≈ 1

You can verify:

print("New means :", np.mean(data_normalized, axis=0))
print("New stds  :", np.std(data_normalized, axis=0))

4.2 Min-Max Normalization (Scaling to [0, 1])

Formula per feature:

[
x' = \frac{x - \min}{\max - \min}
]

Step-by-step with NumPy:

# Same data
data = np.array([
    [1.0,  200.0,  0.5],
    [2.0,  220.0,  0.7],
    [3.0,  240.0,  0.8],
    [4.0,  260.0,  0.9]
])

# 1️⃣ Compute column-wise min and max
data_min = np.min(data, axis=0)
data_max = np.max(data, axis=0)

print("Min:", data_min)
print("Max:", data_max)

# 2️⃣ Apply min-max scaling: (x - min) / (max - min)
data_scaled = (data - data_min) / (data_max - data_min)

print("Scaled data:\n", data_scaled)

Now each feature/column will be in the range [0, 1].

You can verify:

print("Scaled min :", np.min(data_scaled, axis=0))  # should be 0
print("Scaled max :", np.max(data_scaled, axis=0))  # should be 1

When to use which?

  • Z-score normalization

    • Good for algorithms assuming normal distribution or sensitive to scale

    • Example: Linear Regression, Logistic Regression, SVM, Neural Networks

  • Min-Max scaling

    • Good when you want a bounded range (e.g., [0, 1])

    • Often used for normalization before feeding into models or for visualization


If you want, next we can do:

  • Hands-on small exercise:

    • You give a NumPy array, and I’ll ask you to manually compute mean/std for 1 column, then we’ll verify with code.

  • Or move to aggregations like sum, cumsum, prod as the next module.



NumPy Complete Guide: From "What Even Is This?" to Writing ML Code From Scratch

A beginner-to-advanced tutorial that actually makes sense


I still remember the first time I heard someone say "just use NumPy" and I nodded like I knew what that meant. I didn't. I had no idea why a library named after numbers was supposedly the secret to fast Python code, and honestly, the documentation didn't help much.

So I'm writing the tutorial I wish I'd had — one that explains why things work the way they do, not just what the function names are. By the end of this post, you'll understand NumPy well enough to write machine learning algorithms from scratch. No frameworks, no magic — just you and NumPy.

Let's get into it.


What's Covered

  • Module 1: What is NumPy and why should you care?
  • Module 2: Creating arrays (every method you'll actually use)
  • Module 3: Array attributes and data types
  • Module 4: Indexing, slicing, and filtering
  • Module 5: Array operations and vectorization
  • Module 6: Broadcasting — the concept that changes everything
  • Module 7: Reshaping and manipulating arrays
  • Module 8: Stacking and splitting
  • Module 9: Mathematical functions
  • Module 10: Statistical functions and dataset normalization
  • Module 11: Linear algebra with NumPy
  • Module 12: Performance tips that actually matter
  • Module 13: Interview prep — questions and real answers

Module 1: What is NumPy, and Why Does Everyone Use It?

NumPy stands for Numerical Python. It's the core library underneath almost every data science tool in Python — pandas, scikit-learn, TensorFlow, PyTorch — they all build on NumPy.

The central idea is simple: Python lists are flexible, but they're slow for math. If you want to multiply a million numbers by 2, doing it with a Python loop is painfully slow. NumPy exists to fix that.

Why is it so fast?

Three reasons, and these are worth understanding properly.

1. All elements are the same type. A Python list can hold integers, strings, floats, anything — but that flexibility costs performance. NumPy arrays require all elements to be the same type, which removes a ton of overhead.

2. Contiguous memory. Python lists store pointers to objects scattered all over memory. NumPy stores raw data in one continuous block. Your CPU can read that in a single sweep — it's the difference between apartments in one building versus houses spread across a city.

3. The loops run in C. When you write arr * 2 in NumPy, you're not running a Python loop — a compiled C function blasts through the entire array at machine speed.

Here's what the speed difference looks like in practice:

import numpy as np
import time

size = 1_000_000

# Python list
a = list(range(size))
b = list(range(size))

start = time.time()
c = [x + y for x, y in zip(a, b)]
print("Python list:", round(time.time() - start, 4), "seconds")

# NumPy
a_np = np.arange(size)
b_np = np.arange(size)

start = time.time()
c_np = a_np + b_np
print("NumPy:      ", round(time.time() - start, 4), "seconds")

On most machines, NumPy is 50–100x faster. That gap becomes enormous when you're training machine learning models on real data.

Installing and importing

pip install numpy
import numpy as np  # np is the universal alias — don't rename it
print(np.__version__)

The ndarray: the heart of NumPy

Everything in NumPy revolves around the ndarray — an N-dimensional array. You can think of it as a list that only holds one data type, supports any number of dimensions, and does math incredibly fast.

arr_1d = np.array([1, 2, 3])               # 1D — like a list
arr_2d = np.array([[1, 2, 3], [4, 5, 6]])  # 2D — like a spreadsheet
arr_3d = np.array([[[1,2],[3,4]],
                   [[5,6],[7,8]]])           # 3D — like a cube of data

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

Module 2: Creating Arrays

You'll create arrays constantly, so knowing your options saves real time.

From Python lists

# 1D array
arr = np.array([1, 2, 3, 4])

# 2D array (matrix)
matrix = np.array([[1, 2, 3],
                   [4, 5, 6]])

# 3D array
arr3d = np.array([[[1,2],[3,4]],
                  [[5,6],[7,8]]])
print(arr3d.shape)  # (2, 2, 2)

Filled arrays — when you need a starting point

np.zeros((3, 4))         # 3×4 of zeros — great for initializing weight matrices
np.ones((2, 3))          # 2×3 of ones
np.eye(4)                # 4×4 identity matrix — essential for linear algebra
np.full((3, 3), 7)       # every element is 7

Sequences

# arange: step-based, just like Python's range()
np.arange(0, 10, 2)      # [0 2 4 6 8]

# linspace: count-based — give me exactly N values
np.linspace(0, 1, 5)     # [0.   0.25 0.5  0.75 1.  ]

The difference between these two trips people up. arange() lets you control the step size — you get however many values fall in the range. linspace() lets you control how many values you get — NumPy picks the spacing. Use linspace when you need exactly N points, which is common in plots and mathematical calculations.

Random arrays

np.random.rand(3, 4)             # random floats between 0 and 1
np.random.randn(3, 4)            # standard normal distribution (mean=0, std=1)
np.random.randint(0, 100, (5,3)) # random integers from 0 to 99

# Set a seed for reproducibility — always do this in ML
np.random.seed(42)

Module 3: Array Attributes and Data Types

This module sounds dry but it saves you from hours of debugging.

The core attributes

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

arr.shape      # (2, 3)  — rows, columns
arr.ndim       # 2       — number of dimensions
arr.size       # 6       — total element count
arr.dtype      # int64   — data type of elements
arr.itemsize   # 8       — bytes per element
arr.nbytes     # 48      — total memory used (6 elements × 8 bytes)

Data types and why they matter for memory

In machine learning, your choice of dtype can be the difference between a model that fits in RAM and one that doesn't. The same dataset stored as float64 uses twice the memory of float32 — and for most neural networks, float32 precision is more than enough.

# Specify dtype explicitly
arr_int8   = np.array([1, 2, 3], dtype=np.int8)    # 1 byte each
arr_float32 = np.array([1.5, 2.5], dtype=np.float32) # 4 bytes each
arr_float64 = np.array([1.5, 2.5], dtype=np.float64) # 8 bytes each (default)

# Real-world memory comparison
big   = np.ones((1000, 1000), dtype=np.float64)
small = np.ones((1000, 1000), dtype=np.float32)
print(big.nbytes / 1e6, "MB")    # 8.0 MB
print(small.nbytes / 1e6, "MB")  # 4.0 MB — half the RAM!

# Convert between types
arr_float32 = arr_float64.astype(np.float32)

Quick reference for dtypes:

Type Bytes Best For
int8 1 Small integers (-128 to 127)
int32 4 General integers
int64 8 Large integers (default)
float32 4 Deep learning models
float64 8 Scientific computing (default)

View vs. Copy — the #1 source of subtle bugs

When you slice a NumPy array, you don't get a copy of the data. You get a view — a window into the same memory. Modify the view, and you modify the original.

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

# This is a VIEW — modifying b changes arr
b = arr[1:4]
b[0] = 99
print(arr)  # [ 1 99  3  4  5] — arr was modified!

# This is a COPY — modifying c is safe
c = arr.copy()
c[0] = 0
print(arr)  # [ 1 99  3  4  5] — arr unchanged

When in doubt, use .copy(). Silent mutations are some of the hardest bugs to track down.


Module 4: Indexing and Slicing

1D indexing

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

arr[0]      # 10  — first element
arr[-1]     # 50  — last element (negative counts from the end)
arr[1:4]    # [20 30 40] — slice (stop index not included)
arr[::2]    # [10 30 50] — every other element
arr[::-1]   # [50 40 30 20 10] — reverse the entire array

2D indexing

This is where things get interesting. Think of it as [row, column].

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

matrix[0, 1]       # 2 — row 0, column 1
matrix[1]          # [4 5 6] — entire second row
matrix[:, 1]       # [2 5 8] — entire second column (very common!)
matrix[0:2, 1:3]   # [[2 3], [5 6]] — a submatrix

Grabbing an entire column with matrix[:, col_index] is something you'll do constantly in data science.

Boolean indexing — filtering without loops

This is one of those features that, once you know it, you'll use everywhere.

arr = np.array([5, 12, 7, 20, 3, 15])

arr[arr > 10]                    # [12 20 15] — values above 10
arr[arr % 2 == 0]                # [12 20] — even numbers only
arr[(arr > 5) & (arr < 15)]      # [12 7] — multiple conditions (use & not 'and')

# You can also use it to replace values
arr[arr > 10] = 0
print(arr)   # [5  0  7  0  3  0]

Fancy indexing

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

# Select specific elements by index
arr[[0, 2, 4]]   # [10 30 50]

One important distinction: slicing gives a view, fancy indexing gives a copy. This matters for performance and for avoiding accidental mutation.


Module 5: Array Operations and Vectorization

This is the mental shift that separates NumPy beginners from people who actually use it well. You stop writing loops and start thinking in whole arrays.

Element-wise operations

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

a + b   # [5 7 9]   — adds element by element
a * b   # [4 10 18] — multiplies element by element
a ** 2  # [1 4 9]   — squares every element
a / b   # [0.25 0.4 0.5]

Scalar operations

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

arr + 10    # [11 12 13 14 15] — adds 10 to every element
arr * 3     # [3  6  9 12 15]
arr > 3     # [False False False True True]

The axis parameter — crucial for real work

Most aggregation functions accept an axis parameter. Here's the mental model: axis=0 collapses rows (you get one result per column), axis=1 collapses columns (you get one result per row).

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

np.sum(matrix)          # 21    — sum of everything
np.sum(matrix, axis=0)  # [5 7 9]  — sum down each column
np.sum(matrix, axis=1)  # [6 15]   — sum across each row

Matrix multiplication

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

A * B          # element-wise (NOT matrix multiplication)
A @ B          # true matrix multiplication — use this for ML
np.matmul(A, B) # same as @
np.dot(A, B)    # also works for 2D

In machine learning, @ shows up constantly. Every forward pass through a neural network layer is basically a matrix multiplication.


Module 6: Broadcasting — The Concept That Changes Everything

Broadcasting is the feature that makes NumPy code look like magic to newcomers. Once you understand it, it seems obvious — but until then it's confusing. Let's fix that.

What broadcasting actually does

When you perform an operation on two arrays of different shapes, NumPy tries to "expand" the smaller one to match the larger one — without actually copying any data. It's a virtual expansion that happens during computation.

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

b = np.array([10, 20, 30])  # shape (3,)

# b gets virtually expanded to [[10,20,30],[10,20,30]] and added
a + b
# [[11 22 33]
#  [14 25 36]]

The rules (worth memorizing)

NumPy compares shapes from right to left. Two dimensions are compatible if they are equal, or one of them is 1.

(2, 3) + (3,)    → compatible: 3==3       → result (2, 3)  ✓
(4, 3) + (4, 1)  → compatible: 3 vs 1    → result (4, 3)  ✓
(2, 3) + (2, 2)  → incompatible: 3 vs 2  → ERROR          ✗

A real-world example: normalizing a dataset

This is one of the most important operations in data preprocessing. Broadcasting makes it clean and fast.

data = np.array([[10, 200, 3000],
                 [20, 400, 6000],
                 [30, 600, 9000]])

# Compute column-wise mean and std
mean = np.mean(data, axis=0)  # shape (3,)
std  = np.std(data, axis=0)   # shape (3,)

# Normalize — broadcasting subtracts/divides column by column
normalized = (data - mean) / std
print(normalized)

Without broadcasting, you'd need nested loops. With it, three lines. This is the NumPy way.


Module 7: Reshaping and Manipulating Arrays

In machine learning you're constantly reshaping data — to feed into a model, to match what broadcasting expects, or to prepare image tensors. Get comfortable with these.

reshape() — change shape, keep data

arr = np.arange(12)       # [0 1 2 ... 11]

arr.reshape(3, 4)          # 3 rows, 4 columns
arr.reshape(2, -1)         # -1 means "figure out this dimension" → (2, 6)
arr.reshape(-1, 1)         # column vector → (12, 1)
arr.reshape(1, -1)         # row vector → (1, 12)

The total number of elements must stay the same. 12 elements can become (3,4), (2,6), (12,1), etc. — but not (4,4).

flatten() vs ravel()

Both convert a multi-dimensional array to 1D. The difference matters:

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

flat = matrix.flatten()   # always returns a COPY — safe
rav  = matrix.ravel()     # returns a view when possible — faster, but careful

If you modify the result of ravel(), you might modify the original. If you modify the result of flatten(), the original is always safe. When in doubt, use flatten().

transpose()

matrix = np.array([[1,2,3],
                   [4,5,6]])   # shape (2, 3)

matrix.T   # shape (3, 2) — rows and columns swapped

expand_dims() and squeeze()

These come up constantly when preparing data for ML models.

arr = np.array([1, 2, 3])    # shape (3,)

np.expand_dims(arr, axis=0)  # shape (1, 3) — add a row dimension
np.expand_dims(arr, axis=1)  # shape (3, 1) — add a column dimension
arr[:, np.newaxis]           # same as expand_dims with axis=1

# squeeze removes size-1 dimensions
arr_bloated = np.array([[[1,2,3]]])  # shape (1, 1, 3)
np.squeeze(arr_bloated)              # shape (3,)

Common ML scenario: Your model expects input of shape (n_samples, n_features) but you have a single sample with shape (n_features,). The fix is one line:

x = np.array([1, 2, 3])   # shape (3,)
x = x.reshape(1, -1)      # shape (1, 3) ← model-ready

Module 8: Stacking and Splitting Arrays

Think of stacking as combining spreadsheets. Splitting is the reverse. Both show up constantly in ML pipelines.

Stacking

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

np.vstack((a, b))                   # [[1 2 3], [4 5 6]] — add rows
np.hstack((a, b))                   # [[1 2 3 4 5 6]] — add columns
np.concatenate((a, b), axis=0)      # same as vstack
np.concatenate((a, b), axis=1)      # same as hstack

Splitting

arr = np.arange(12)

parts = np.split(arr, 3)        # 3 equal parts of 4 elements each

matrix = np.arange(12).reshape(4, 3)
np.vsplit(matrix, 2)             # split into 2 matrices by row
np.hsplit(matrix, 3)             # split into 3 parts by column

When to use which: hstack for combining features, vstack for combining datasets, split for creating train/test/validation splits.


Module 9: Mathematical Functions

All of these work element-wise — no loops needed.

arr = np.array([1, 4, 9, 16])

np.sqrt(arr)            # [1. 2. 3. 4.]
np.abs([-3, -1, 2])     # [3 1 2]
np.power(arr, 2)        # same as arr**2
np.log(arr)             # natural log
np.log10(arr)           # log base 10
np.exp(arr)             # e^x

# Trigonometric — NumPy uses radians, not degrees
degrees = np.array([0, 90, 180])
radians = np.deg2rad(degrees)

np.sin(radians)         # [0. 1. 0.]
np.cos(radians)         # [1. 0. -1.]
np.tanh(arr)            # hyperbolic tangent — the tanh activation function

These matter in ML. The sigmoid activation function is 1 / (1 + np.exp(-x)). The tanh activation is np.tanh(x). You can implement any activation function from scratch with these tools.


Module 10: Statistical Functions and Normalization

Core statistical functions

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

np.mean(data)             # 5.0 — overall mean
np.mean(data, axis=0)     # [4. 5. 6.] — mean of each column
np.mean(data, axis=1)     # [2. 5. 8.] — mean of each row

np.median(data)           # 5.0
np.std(data, axis=0)      # standard deviation per column
np.var(data)              # variance (std squared)

np.min(data, axis=0)      # minimum of each column
np.max(data, axis=1)      # maximum of each row

np.argmin(data, axis=0)   # INDEX of the column minimum
np.argmax(data, axis=1)   # INDEX of the row maximum

Practice: Z-score normalization (standardization)

This is one of the most common preprocessing steps in all of ML. It transforms each feature to have mean = 0 and standard deviation = 1.

# Dataset: 4 samples, 3 features
data = np.array([
    [1.0,  200.0,  0.5],
    [2.0,  220.0,  0.7],
    [3.0,  240.0,  0.8],
    [4.0,  260.0,  0.9]
])

# Step 1: column-wise statistics
mean = np.mean(data, axis=0)   # shape (3,)
std  = np.std(data, axis=0)    # shape (3,)

# Step 2: normalize — broadcasting handles the rest
normalized = (data - mean) / std

# Verify it worked
print(np.mean(normalized, axis=0))  # [0. 0. 0.] ✓
print(np.std(normalized, axis=0))   # [1. 1. 1.] ✓

Practice: Min-max normalization (scale to 0–1)

data_min = np.min(data, axis=0)
data_max = np.max(data, axis=0)

scaled = (data - data_min) / (data_max - data_min)

print(np.min(scaled, axis=0))  # [0. 0. 0.] ✓
print(np.max(scaled, axis=0))  # [1. 1. 1.] ✓

When to use which: Z-score works best for linear models, SVMs, and neural networks. Min-max works well for CNN inputs, image data, and visualizations.


Module 11: Linear Algebra with NumPy

You don't need to memorize every operation here. Focus on the ones that come up in ML constantly.

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

# Dot product (vectors)
np.dot([1, 2], [3, 4])         # 11  (1×3 + 2×4)

# Matrix multiplication
A @ B

# Transpose
A.T

# Determinant
np.linalg.det(A)

# Matrix inverse
np.linalg.inv(A)

# Eigenvalues and eigenvectors (used in PCA)
eigenvalues, eigenvectors = np.linalg.eig(A)

# SVD (powers recommender systems)
U, S, Vt = np.linalg.svd(A)

# Vector norm (Euclidean length)
np.linalg.norm([3, 4])         # 5.0

Eigenvalues and eigenvectors power PCA. SVD powers recommender systems. Matrix inversion appears in the closed-form solution for linear regression. These aren't just textbook math — they're the actual machinery behind real ML systems.


Module 12: Performance Tips That Actually Matter

The golden rule: no Python loops

This is the single biggest performance improvement you can make.

# Slow — Python loop
result = []
for x in arr:
    result.append(x ** 2)

# Fast — vectorized
result = arr ** 2

In-place operations save memory

# Creates a new array (uses more RAM)
arr = arr + 10

# Modifies the original (no new allocation)
arr += 10

Choose the right dtype

data = np.random.rand(1_000_000)
print(data.nbytes / 1e6, "MB")        # 8.0 MB as float64

data = data.astype(np.float32)
print(data.nbytes / 1e6, "MB")        # 4.0 MB — half the RAM, same results

Benchmarking your code

import time

arr = np.arange(1_000_000)

start = time.time()
result = np.sum(arr)
print(f"Vectorized: {time.time() - start:.6f}s")

start = time.time()
result = sum(list(arr))
print(f"Loop:       {time.time() - start:.6f}s")

Module 13: Interview Prep

If you're preparing for a data science or ML role, these questions come up constantly. Here are real answers, not just definitions.


Q: Why is NumPy faster than Python lists?

Four reasons: homogeneous dtype removes type-checking overhead, contiguous memory storage makes CPU cache access efficient, operations are implemented in compiled C code, and vectorization sends entire array operations to optimized routines without Python-level loops.


Q: What's the difference between flatten() and ravel()?

flatten() always returns a copy of the data — safe, but uses more memory. ravel() returns a view when possible — faster and more memory-efficient, but modifying the result may modify the original. Use flatten() when safety matters, ravel() when you're optimizing performance and you know what you're doing.


Q: What's the difference between reshape() and resize()?

reshape() returns a new view (or copy) without touching the original array, and the total element count must stay the same. resize() modifies the array in-place and can change the total size — padding with zeros if you grow it, or truncating if you shrink it.


Q: What is broadcasting?

Broadcasting lets NumPy perform operations on arrays of different shapes without copying data. It virtually expands the smaller array to match the larger one. The rules: compare shapes from right to left, and dimensions are compatible if they're equal or one of them is 1.


Q: What does the axis parameter mean?

The axis along which an operation is applied. In a 2D array: axis=0 collapses rows and gives you one result per column, axis=1 collapses columns and gives you one result per row. Think of it as "which dimension gets reduced."


Q: What's the difference between * and @?

* is element-wise multiplication — corresponding elements get multiplied together. @ is matrix multiplication — follows linear algebra rules where the inner dimensions must match. In ML, you almost always want @.


Q: When does slicing give a view vs. a copy?

Slicing (like arr[1:4]) gives a view — the same underlying data, just accessed differently. Fancy indexing (using arrays or lists as indices, like arr[[1,2,4]]) gives a copy. When you call .copy() explicitly, you always get a copy. You can check with arr.base is None — if True, it's a copy; if False, it's a view.


Q: Why is float64 the default for floating-point arrays?

NumPy defaults to float64 because it offers about 15 decimal digits of precision, which is appropriate for scientific and numerical computing where precision matters. In ML, you typically downcast to float32 intentionally to halve memory usage — the reduced precision is rarely an issue for model training.


The 8-Week Path to Actually Knowing NumPy

One last thing before you go. A lot of people read tutorials like this, nod along, and then open a blank notebook and freeze. Here's a plan that actually works:

Weeks 1–2: Work through Modules 1–4. Create arrays, index them, understand dtypes and views. Write small practice scripts every day, don't just read.

Weeks 3–4: Modules 5–8. Start writing vectorized code. Every time you feel the urge to write a for-loop, stop and think "can I do this with array operations?" Usually yes.

Weeks 5–6: Modules 9–12. Get comfortable with linear algebra. Implement a few math formulas from scratch — Euclidean distance, cosine similarity, normalization. Time your code and see how vectorization compares to loops.

Week 7: Apply it to ML. Implement linear regression with gradient descent using only NumPy. Build a neural network forward pass. These will cement every concept in this guide.

Week 8: Projects and interview prep. Go through Module 13 out loud — actually say the answers. Then build something: K-means clustering, a simple recommender system, image normalization. Code you ship sticks with you longer than code you read.

NumPy rewards time spent with it. The more you use it, the more instinctive it becomes. And once you can think in arrays, the whole Python data science ecosystem opens up.








Comments

Popular posts from this blog

Python Tutorial with examples

Machine learning tutorial

RAG (Retrieval-Augmented Generation) tutorial