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
shapendimsizedtypeitemsizenbytes
📌 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
meanmedianstdvarminmaxargminargmaxAxis 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()baseattributeMemory 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,float64Type 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
| Feature | List | ndarray |
|---|---|---|
| Mixed types | Yes | No |
| Memory efficient | No | Yes |
| Fast math | No | Yes |
| Vectorized | No | Yes |
🔹 Array vs Matrix
NumPy recommends:
Use ndarray not np.matrix.
Because:
matrixis olderLess 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:
Homogeneous data
Contiguous memory
C implementation
Vectorization
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 steplinspace()→ 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
| Function | Use 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:
Data buffer → Actual stored values
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
| Type | Bytes |
|---|---|
| int8 | 1 |
| int16 | 2 |
| int32 | 4 |
| int64 | 8 |
Example:
arr = np.array([1,2,3], dtype=np.int8)
print(arr.dtype)
🔹 Float Types
| Type | Precision |
|---|---|
| float16 | Low |
| float32 | Medium |
| float64 | High (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
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]
print(arr[:3]) # [10 20 30]
🔹 Till End
print(arr[2:]) # [30 40 50]
print(arr[2:]) # [30 40 50]
🧠 4️⃣ Step Slicing
Syntax:
array[start:stop:step]
🔹 Skip Values
print(arr[::2]) # [10 30 50]
print(arr[::2]) # [10 30 50]
🔹 Reverse Array
print(arr[::-1]) # [50 40 30 20 10]
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
print(matrix[0, 1]) # 2
Meaning:
Row 0
Column 1
🔹 Access Full Row
print(matrix[1]) # [4 5 6]
print(matrix[1]) # [4 5 6]
or
print(matrix[1, :])
🔹 Access Full Column
print(matrix[:, 1]) # [2 5 8]
print(matrix[:, 1]) # [2 5 8]
Meaning:
All rows
Column 1
🧠 6️⃣ 2D Slicing
🔹 Extract Submatrix
print(matrix[0:2, 1:3])
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])
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])
print(arr[arr % 2 == 0])
🔹 2D Boolean Filtering
matrix = np.array([
[5, 12, 7],
[20, 3, 15]
])
print(matrix[matrix > 10])
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])
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])
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)
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])
matrix = np.array([
[5, 12, 7],
[20, 3, 15]
])
print(matrix[matrix > 10])
✅ Practice 5: Replace Values > 10
matrix[matrix > 10] = 999
print(matrix)
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)
print(a + b)
Output:
[5 7 9]
🔹 Subtraction
print(a - b)
print(a - b)
🔹 Multiplication
print(a * b)
print(a * b)
🔹 Division
print(a / b)
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)
print(a > 2)
Output:
[False False True]
Use with Filtering
print(a[a > 1])
print(a[a > 1])
Output:
[2 3]
🧠 5️⃣ Logical Operations
a = np.array([1, 2, 3, 4])
print((a > 1) & (a < 4))
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))
arr = np.array([1, 2, 3, 4])
print(np.sum(arr))
🔹 Mean
print(np.mean(arr))
print(np.mean(arr))
🔹 Min / Max
print(np.min(arr))
print(np.max(arr))
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))
print(np.sum(matrix, axis=0))
Output:
[5 7 9]
Row-wise sum
print(np.sum(matrix, axis=1))
print(np.sum(matrix, axis=1))
Output:
[6 15]
🧠 8️⃣ Matrix Multiplication vs Element-wise
🔹 Element-wise
a * b
a * b
🔹 Dot Product
np.dot(a, b)
np.dot(a, b)
🔹 Matrix Multiplication
A = np.array([[1,2],[3,4]])
B = np.array([[5,6],[7,8]])
print(A @ B)
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)
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)
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])
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))
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)
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:
Compare shapes from right to left
Two dimensions are compatible if:
They are equal
OR one of them is 1
Example 1
(2,3)
(3,)
(2,3)
(3,)
Compare:
(2,3)
(1,3)
Compatible ✅
Example 2
(2,3)
(2,1)
(2,3)
(2,1)
Result shape → (2,3)
Example 3 (Error)
(2,3)
(2,2)
(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)
arr = np.arange(6)
reshaped = arr.reshape(2,3)
Important:
Total elements must match.
🧠 2️⃣ flatten() vs ravel()
flatten()
Returns copy
Returns copy
ravel()
Returns view (if possible)
Returns view (if possible)
🧠 3️⃣ transpose()
matrix = np.array([[1,2],[3,4]])
print(matrix.T)
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))
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))
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))
a = np.array([1,2])
b = np.array([3,4])
print(np.dot(a,b))
🧠 2️⃣ Matrix Multiplication
A @ B
A @ B
🧠 3️⃣ Determinant
np.linalg.det(A)
np.linalg.det(A)
🧠 4️⃣ Inverse
np.linalg.inv(A)
np.linalg.inv(A)
🧠 5️⃣ Eigenvalues
np.linalg.eig(A)
np.linalg.eig(A)
Used in:
PCA
ML
Signal processing
🧠 6️⃣ Norm
np.linalg.norm(A)
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)
np.array([1,2,3], dtype=np.int8)
Reduces memory usage.
🔥 4️⃣ In-place Operations
arr += 10
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
reshape → returns new view (if possible)
resize → modifies original array
Q3: Difference between flatten and ravel?
flatten → copy
ravel → view (if possible)
flatten → copy
ravel → view (if possible)
Q4: Difference between dot and matmul?
dot → vector dot + matrix multiplication
matmul (@) → strict matrix multiplication
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
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,)
(2,3)
(3,)
Rewrite as:
(2,3)
(1,3)
Compatible ✅
Result shape → (2,3)
✅ Example 2
(4,3)
(4,1)
(4,3)
(4,1)
Compare:
3 vs 1 → OK
4 vs 4 → OK
Result shape → (4,3)
❌ Example 3 (Error)
(2,3)
(2,2)
(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)
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)
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)
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)
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
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
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
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)
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)
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)
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)
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)
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)
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)
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)
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
| Feature | flatten() | ravel() |
|---|---|---|
| Copy | Yes | Usually No |
| Memory efficient | No | Yes |
| Safe | Yes | Might affect original |
🧠 reshape() vs transpose()
| reshape | transpose |
|---|---|
| Changes shape | Swaps axes |
| Data order preserved | Data orientation changed |
🧠 Common Errors
❌ Wrong reshape size
arr.reshape(4,4)
arr.reshape(4,4)
Error if total elements mismatch.
❌ Confusing 1D vs 2D
arr = np.array([1,2,3])
arr.T
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)
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)
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))
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)
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]]
[[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)))
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))
np.hstack((arr1, arr2))
🔹 Example:
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
result = np.hstack((a, b))
print(result)
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]
[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)))
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)
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))
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))
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)
np.split(array, sections, axis=0)
🔹 Example:
a = np.array([1,2,3,4,5,6])
print(np.split(a, 3))
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))
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))
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))
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
| Function | Direction | Works On |
|---|---|---|
vstack() | Add rows | Stack vertically |
hstack() | Add columns | Stack horizontally |
concatenate() | Custom | axis=0 or 1 |
split() | Split generic | axis control |
hsplit() | Split columns | Horizontal |
vsplit() | Split rows | Vertical |
🚀 Interview + ML Insight
In ML pipelines:
hstack()→ Combine featuresvstack()→ Combine datasetsconcatenate()→ Flexible mergingsplit()→ Train/test splittinghsplit()→ Feature separationvsplit()→ 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)
np.sqrt(array)
✅ Example:
import numpy as np
arr = np.array([1, 4, 9, 16])
print(np.sqrt(arr))
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
Distance calculation (Euclidean distance)
Standard deviation formula
🔹 2️⃣ Exponential – np.exp()
Computes e^x (Euler’s number ≈ 2.718).
✅ Syntax:
np.exp(array)
np.exp(array)
✅ Example:
arr = np.array([1, 2, 3])
print(np.exp(arr))
arr = np.array([1, 2, 3])
print(np.exp(arr))
📌 Use Case:
Logistic regression (sigmoid function)
Neural networks
Growth models
Logistic regression (sigmoid function)
Neural networks
Growth models
🔹 3️⃣ Logarithm – np.log()
Computes natural log (base e).
✅ Syntax:
np.log(array)
np.log(array)
Example:
arr = np.array([1, np.e, 10])
print(np.log(arr))
arr = np.array([1, np.e, 10])
print(np.log(arr))
Other log variants:
np.log10(arr) # Base 10
np.log2(arr) # Base 2
np.log10(arr) # Base 10
np.log2(arr) # Base 2
📌 Use Case:
Log loss
Data normalization
Financial calculations
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)
np.power(base, exponent)
Example:
arr = np.array([2, 3, 4])
print(np.power(arr, 2))
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))
arr = np.array([-5, -1, 3])
print(np.abs(arr))
Output:
[5 1 3]
📌 Use Case:
Error calculation (MAE)
Distance formulas
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))
angles = np.array([0, np.pi/2, np.pi])
print(np.sin(angles))
print(np.cos(angles))
📍 Convert Degrees to Radians
np.deg2rad(90)
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))
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))
arr = np.array([0, 1])
print(np.tanh(arr))
📌 Used in:
Neural networks (tanh activation)
🔥 Summary Table
| Function | Description |
|---|---|
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)
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)
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)
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)
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)
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)
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)
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)
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
| Feature | flatten() | ravel() |
|---|---|---|
| Copy | Yes | Usually No |
| Memory efficient | No | Yes |
| Safe | Yes | Might affect original |
🧠 reshape() vs transpose()
| reshape | transpose |
|---|---|
| Changes shape | Swaps axes |
| Data order preserved | Data orientation changed |
🧠 Common Errors
❌ Wrong reshape size
arr.reshape(4,4)
arr.reshape(4,4)
Error if total elements mismatch.
❌ Confusing 1D vs 2D
arr = np.array([1,2,3])
arr.T
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)
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)
arr.reshape(-1,1)
📌 Module 7 Summary
You mastered:
✔ reshape()
✔ flatten()
✔ ravel()
✔ transpose()
✔ swapaxes()
✔ expand_dims()
✔ squeeze()
✔ Real ML shape handling
mean,median,std,var,min,max,argmin,argmaxaxisparameter (very important)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.0np.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.0np.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
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)→1np.max(a)→9np.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
ais[1,2,3,4,5,6,7,8,9]np.argmin(a)→0(value1)np.argmax(a)→8(value9)
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:
axisis 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:
Z-score normalization (standardization) – uses
meanandstdMin-Max scaling – uses
minandmax
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 broadcastsmeanacross 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
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,prodas 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__)
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)
# 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
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. ]
# 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)
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)
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
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]
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]
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]
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
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)
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
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
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
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
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.] ✓
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
# 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
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")
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
Post a Comment