Numpy
A structured NumPy practice repository showcasing hands-on learning through Jupyter notebooks. It covers array creation, indexing, slicing, reshaping, broadcasting, and numerical operations. Built to strengthen core foundations required for Data Science, Machine Learning, and efficient scientific computing in Python.
Install / Use
/learn @mdzaheerjk/NumpyREADME
NumPy Complete Reference Notes
What is NumPy?
NumPy (Numerical Python) is the foundational library for numerical computing in Python. It provides:
- ndarray — a fast, memory-efficient N-dimensional array
- Vectorised math operations (no Python loops needed)
- Broadcasting — operating on arrays of different shapes
- Linear algebra, FFT, random number generation
- The backbone of pandas, SciPy, TensorFlow, scikit-learn, and PyTorch
pip install numpy
import numpy as np
The ndarray — Core Data Structure
Every NumPy array is an ndarray: a typed, fixed-size, contiguous block of memory.
Key attributes
a = np.array([[1, 2, 3], [4, 5, 6]])
a.ndim # 2 — number of dimensions (axes)
a.shape # (2, 3) — tuple of sizes per axis
a.size # 6 — total number of elements
a.dtype # int64 — element data type
a.itemsize # 8 — bytes per element
a.nbytes # 48 — total bytes (size × itemsize)
a.strides # (24, 8) — bytes to step along each axis
Creating Arrays
From Python sequences
np.array([1, 2, 3]) # 1-D array
np.array([[1, 2], [3, 4]]) # 2-D array (matrix)
np.array([1.0, 2, 3]) # dtype inferred as float64
np.array([1, 2, 3], dtype=np.int32) # explicit dtype
Constant arrays
np.zeros((3, 4)) # 3×4 array of 0.0 (float64)
np.ones((2, 3)) # 2×3 array of 1.0
np.full((3, 3), 7) # 3×3 array filled with 7
np.empty((2, 2)) # uninitialised (fast, garbage values)
np.eye(4) # 4×4 identity matrix
np.identity(4) # same as eye(4)
np.zeros_like(a) # zeros with same shape/dtype as a
np.ones_like(a)
np.full_like(a, fill_value=0)
Range & sequence arrays
np.arange(10) # [0 1 2 ... 9] (like range())
np.arange(1, 10, 2) # [1 3 5 7 9] start, stop, step
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1.] — n evenly spaced
np.linspace(0, 1, 5, endpoint=False) # exclude endpoint
np.logspace(0, 3, 4) # [1. 10. 100. 1000.] — log spacing
np.geomspace(1, 1000, 4) # [1. 10. 100. 1000.] — geometric spacing
Grid arrays
np.meshgrid([1, 2, 3], [4, 5]) # two 2-D grids (broadcasting helper)
np.mgrid[0:3, 0:4] # open mesh grid via slice notation
np.ogrid[0:3, 0:4] # open (sparse) mesh grid
np.indices((3, 4)) # array of indices
Special arrays
np.diag([1, 2, 3]) # diagonal matrix from 1-D
np.diag(A) # extract diagonal from 2-D
np.tri(3, 4, k=0) # lower-triangular matrix
np.triu(A) # upper triangle of A (in-place zero)
np.tril(A) # lower triangle of A
np.vander([1, 2, 3], 4) # Vandermonde matrix
Data Types (dtype)
| Type | Alias | Bytes | Notes |
|------|-------|-------|-------|
| np.int8 | i1 | 1 | |
| np.int16 | i2 | 2 | |
| np.int32 | i4 | 4 | |
| np.int64 | i8 | 8 | Default int on 64-bit |
| np.uint8 | u1 | 1 | Unsigned (images!) |
| np.uint16–uint64 | u2–u8 | 2–8 | |
| np.float16 | f2 | 2 | Half precision |
| np.float32 | f4 | 4 | GPU-friendly |
| np.float64 | f8 | 8 | Default float |
| np.complex64 | c8 | 8 | |
| np.complex128 | c16 | 16 | Default complex |
| np.bool_ | ? | 1 | |
| np.str_ | U | varies | Fixed-length Unicode |
| np.object_ | O | ptr | Python object |
a = np.array([1, 2, 3], dtype=np.float32)
b = a.astype(np.int16) # cast to new dtype (copies data)
np.can_cast(np.int32, np.float64) # True — safe cast check
np.result_type(np.int32, np.float64) # float64 — common type
Indexing & Slicing
Basic indexing
a = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
a[0] # [10 20 30] — first row
a[0, 1] # 20 — row 0, col 1
a[-1] # [70 80 90] — last row
a[1, -1] # 60 — row 1, last col
Slicing [start:stop:step]
a[0:2] # rows 0 and 1
a[:, 1] # all rows, col 1 → [20 50 80]
a[::2, ::2] # every other row and col
a[1:, :2] # rows 1+, cols 0-1
a[::-1] # reversed rows
Slices return views (no copy). Modifying a slice modifies the original.
b = a[0:2, 0:2]
b[0, 0] = 999 # a[0, 0] is now 999
b = a[0:2, 0:2].copy() # force a copy
Boolean (mask) indexing
a = np.array([1, 5, 3, 8, 2])
mask = a > 3 # [False True False True False]
a[mask] # [5 8]
a[a > 3] # same, inline
# Multi-condition
a[(a > 2) & (a < 8)] # & | ~ for and/or/not (not Python and/or)
a[np.logical_and(a > 2, a < 8)]
Fancy (integer) indexing
a = np.array([10, 20, 30, 40, 50])
a[[0, 2, 4]] # [10 30 50] — select by index list
a[[3, 3, 1]] # [40 40 20] — repetition allowed
# 2-D fancy indexing
M = np.arange(12).reshape(3, 4)
M[[0, 2], [1, 3]] # [M[0,1], M[2,3]] = [1, 11]
# np.ix_ for selecting submatrices
M[np.ix_([0, 2], [1, 3])] # 2×2 submatrix at rows {0,2} × cols {1,3}
Fancy indexing returns copies, not views.
np.where
np.where(a > 3) # indices where condition is True
np.where(a > 3, a, 0) # elementwise ternary: a if a>3 else 0
np.where(a > 3, "big", "small")
Reshaping & Manipulation
a = np.arange(12)
a.reshape(3, 4) # 3×4 — returns a view when possible
a.reshape(3, -1) # -1 infers the missing dimension (= 4)
a.reshape(2, 2, 3) # 3-D
a.ravel() # flatten to 1-D (view when possible)
a.flatten() # flatten to 1-D (always a copy)
a.T # transpose (view)
a.transpose(1, 0, 2) # reorder axes explicitly
np.expand_dims(a, axis=0) # insert axis: (12,) → (1, 12)
np.squeeze(a) # remove size-1 axes
a[:, np.newaxis] # insert axis with np.newaxis
a[np.newaxis, :] # shape (1, 12)
Stacking & splitting
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
np.concatenate([a, b]) # [1 2 3 4 5 6]
np.concatenate([A, B], axis=0) # stack rows (vstack)
np.concatenate([A, B], axis=1) # stack cols (hstack)
np.vstack([a, b]) # vertical stack → 2-D
np.hstack([a, b]) # horizontal stack
np.dstack([a, b]) # depth stack (3rd axis)
np.stack([a, b], axis=0) # new axis; shape (2, 3)
np.column_stack([a, b]) # 1-D arrays become columns
np.split(a, 3) # split into 3 equal parts
np.split(a, [2, 5]) # split at indices 2 and 5
np.vsplit(A, 2)
np.hsplit(A, 2)
np.array_split(a, 4) # unequal split (no error)
Repeating & tiling
np.repeat([1, 2], 3) # [1 1 1 2 2 2]
np.repeat(A, 2, axis=0) # repeat each row twice
np.tile([1, 2], 3) # [1 2 1 2 1 2]
np.tile(A, (2, 3)) # tile the whole array 2×3 times
Broadcasting
Broadcasting lets NumPy operate on arrays of compatible but different shapes without copying data.
Rules (applied left-to-right on shape tuples, padded with 1s on the left)
- Shapes are equal, OR
- One of them is 1 (that dimension is "stretched")
# Shape (3,) + (3, 3) → (3, 3)
row = np.array([1, 2, 3])
M = np.ones((3, 3))
M + row # adds row to every row of M
# Shape (3, 1) + (1, 4) → (3, 4)
col = np.arange(3).reshape(3, 1)
row = np.arange(4).reshape(1, 4)
col + row # outer-sum: 3×4 matrix
# Practical: normalise each row to zero mean
X = np.random.randn(100, 5)
X -= X.mean(axis=0) # X.mean shape (5,) broadcasts over rows
X /= X.std(axis=0)
Universal Functions (ufuncs)
Ufuncs are vectorised C-level functions. They operate element-wise without Python loops and support broadcasting, reduceat, accumulate, and out= arguments.
Math ufuncs
np.add(a, b) # a + b
np.subtract(a, b) # a - b
np.multiply(a, b) # a * b
np.divide(a, b) # a / b (true divide)
np.floor_divide(a, b) # a // b
np.mod(a, b) # a % b
np.power(a, b) # a ** b
np.negative(a) # -a
np.absolute(a) # |a| — also np.abs
np.sign(a) # -1, 0, or 1
np.sqrt(a)
np.square(a)
np.cbrt(a) # cube root
np.reciprocal(a) # 1/a
Exponential & logarithmic
np.exp(a) # e^a
np.exp2(a) # 2^a
np.log(a) # natural log
np.log2(a)
np.log10(a)
np.log1p(a) # log(1 + a) — more accurate near zero
np.expm1(a) # e^a - 1 — more accurate near zero
Trigonometric
np.sin(a); np.cos(a); np.tan(a)
np.arcsin(a); np.arccos(a); np.arctan(a)
np.arctan2(y, x) # angle of (x,y) respecting quadrant
np.hypot(a, b) # sqrt(a² + b²)
np.deg2rad(a); np.rad2deg(a)
np.sinh(a); np.cosh(a); np.tanh(a)
np.arcsinh(a); np.arccosh(a); np.arctanh(a)
Comparison & logical ufuncs
np.equal(a, b) # a == b
np.not_equal(a, b) # a != b
np.greater(a, b) # a > b
np.less_equal(a, b) # a <= b
np.logical_and(a, b)
np.logical_or(a, b)
np.logical_not(a)
np.logical_xor(a, b)
Ufunc methods
np.add.reduce(a) # sum along axis 0 (same as np.sum)
np.add.accumulate(a) # running cumulative sum
np.multiply.reduce(a) # product
np.add.outer(a, b) # outer product: a[:, None] + b[None, :]
np.add.reduceat(a, [0,3]) # reduce over slices defined by indices
Aggregation & Reduction
