Lectures: Nguyễn Tuấn Phong, Đào Việt Anh
Labs: Trịnh Ngọc Huỳnh, Phạm Tiến Du
Semester: 2025-2026-1
Institute for AI, VNU-UET
import numpy as np
my_arr = np.arange(1_000_000)
my_list = list(range(1_000_000))
%timeit my_arr * 2
366 μs ± 1.94 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%timeit [x * 2 for x in my_list]
43.9 ms ± 260 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
💡 NumPy is >100× faster than lists in this operation!
import numpy as np
data = np.array([[1.5, -0.1, 3], [0, -3, 6.5]])
data
array([[ 1.5, -0.1, 3. ],
[ 0. , -3. , 6.5]])
data * 10
array([[ 15., -1., 30.],
[ 0., -30., 65.]])
data + data
array([[ 3. , -0.2, 6. ],
[ 0. , -6. , 13. ]])
data.shape, data.dtype
((2, 3), dtype('float64'))
Simple list → 1D array:
arr1 = np.array([6, 7.5, 8, 0, 1])
arr1
array([6. , 7.5, 8. , 0. , 1. ])
Nested list → 2D array:
arr2 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
arr2.shape, arr2.ndim
((2, 4), 2)
NumPy tries to infer data types automatically:
arr1.dtype, arr2.dtype
(dtype('float64'), dtype('int64'))
np.zeros((2,6))
array([[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]])
np.ones((3,4), dtype=np.int32)
array([[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]], dtype=int32)
empty_array = np.empty((2,3,2))
empty_array
array([[[1.5e-323, 0.0e+000],
[0.0e+000, 0.0e+000],
[0.0e+000, 0.0e+000]],
[[0.0e+000, 0.0e+000],
[0.0e+000, 0.0e+000],
[0.0e+000, 0.0e+000]]])
np.arange(10)
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
For more special constructors, see:
Table: NumPy array creation
functions
arr1 = np.array([1, 2, 3], dtype=np.float64)
arr2 = np.array([1, 2, 3], dtype=np.int32)
arr1.dtype, arr2.dtype
(dtype('float64'), dtype('int32'))
Integers to floats
arr = np.array([1, 2, 3, 4, 5])
arr.astype(np.float64)
array([1., 2., 3., 4., 5.])
Strings to numeric
np.array(["1.25", "-9.6", "42"]).astype(float)
array([ 1.25, -9.6 , 42. ])
Arithmetic operations on arrays apply element-wise
Between arrays of the same size:
arr = np.array([[1., 2., 3.],[4., 5., 6.]])
arr * arr
array([[ 1., 4., 9.],
[16., 25., 36.]])
arr - arr
array([[0., 0., 0.],
[0., 0., 0.]])
Arithmetic operations on arrays apply element-wise
Between arrays and scalars:
1 / arr
array([[1. , 0.5 , 0.33333333],
[0.25 , 0.2 , 0.16666667]])
arr ** 2
array([[ 1., 4., 9.],
[16., 25., 36.]])
Comparisons between arrays of the same size yield Boolean arrays:
arr2 = np.array([[0.,4.,1.],[7.,2.,12.]])
arr2 > arr
array([[False, True, False],
[ True, False, True]])
arr = np.arange(10)
arr[5], arr[5:8]
(5, array([5, 6, 7]))
arr[5:8] = 12
arr
array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
arr_slice = arr[5:8]
arr_slice
array([12, 12, 12])
arr_slice[1] = 12345
arr
array([ 0, 1, 2, 3, 4, 12, 12345,
12, 8, 9])
Use : to modify the entire array:
arr_slice[:] = 64
arr
array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
Use .copy() to make a copy of a slice
arr_copy = arr[5:8].copy()
arr_copy
array([64, 64, 64])
arr_copy[1] = 12345
arr
array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
Elements at each index are one-dimensional arrays
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
arr2d[2]
array([7, 8, 9])
Individual elements can be accessed recursively:
arr2d[0][2]
np.int64(3)
Or using a comma-separated tuple of indices:
arr2d[0, 2]
np.int64(3)
Think of axis 0 as the rows and axis 1 as the columns.
arr3d = np.array([[[ 1, 2, 3], [ 4, 5, 6]],
[[ 7, 8, 9], [10, 11, 12]]])
arr3d
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d.shape
(2, 2, 3)
Omitting later indices → return a lower dimensional ndarray
arr3d[0] # a 2x3 array
array([[1, 2, 3],
[4, 5, 6]])
arr3d[1, 0] # returns 1D array
array([7, 8, 9])
old_values = arr3d[0].copy()
Assign a scalar value:
arr3d[0] = 42
arr3d
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
From another array:
arr3d[0] = old_values
arr3d
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
1D arrays can be sliced with the : operator.
arr = np.array([0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
arr
array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
arr[1:6]
array([ 1, 2, 3, 4, 64])
2D arrays can be sliced along both axes.
arr2d = np.arange(1, 10).reshape(3, 3)
arr2d
arr2d[:2] # first two rows (slice along axis 0)
arr2d[:2, 1:] # rows 0-1, cols 1-end
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
array([[1, 2, 3],
[4, 5, 6]])
array([[2, 3],
[5, 6]])
Can mix integer indices and slices.
lower_dim_slice = arr2d[1, :2] # 2nd row, first 2 columns
lower_dim_slice.shape
arr2d[:2, 2] # 3rd column of first 2 rows
arr2d[:, :1] # first column, all rows
(2,)
array([3, 6])
array([[1],
[4],
[7]])
Assignments to slices propagate to the entire selection.
arr2d[:2, 1:] = 0
arr2d
array([[1, 0, 0],
[4, 0, 0],
[7, 8, 9]])
names = np.array(["Bob", "Joe", "Will", "Bob", "Will",
"Joe", "Joe"])
names
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
data = np.array([[4, 7], [0, 2], [-5, 6], [0, 0], [1, 2],
[-12, -4], [3, 4]])
data
array([[ 4, 7],
[ 0, 2],
[ -5, 6],
[ 0, 0],
[ 1, 2],
[-12, -4],
[ 3, 4]])
Comparisons are vectorized:
names == "Bob"
array([ True, False, False, True, False, False, False])
→ This array of Boolean values is called a mask.
Mask length must match the axis being indexed
data[names == "Bob"] # rows where name is Bob
array([[4, 7],
[0, 0]])
data[names == "Bob", 1:] # from the 2nd column onward
array([[7],
[0]])
data[names == "Bob", 1] # only the 2nd column
array([7, 0])
names != "Bob"
array([False, True, True, False, True, True, True])
~(names == "Bob")
array([False, True, True, False, True, True, True])
data[~(names == "Bob")]
array([[ 0, 2],
[ -5, 6],
[ 1, 2],
[-12, -4],
[ 3, 4]])
Use Boolean arithmetic operators: & (and), | (or).
mask = (names == "Bob") | (names == "Will")
mask
array([ True, False, True, True, True, False, False])
data[mask]
array([[ 4, 7],
[-5, 6],
[ 0, 0],
[ 1, 2]])
Note: Boolean selection returns a copy of the data.
Right-hand values broadcast into True positions.
data[data < 0] = 0
data
array([[4, 7],
[0, 2],
[0, 6],
[0, 0],
[1, 2],
[0, 0],
[3, 4]])
Right-hand values broadcast into True positions.
data[names != "Joe"] = 7
data
array([[7, 7],
[0, 2],
[7, 7],
[7, 7],
[7, 7],
[0, 0],
[3, 4]])
Fancy indexing = indexing with integer arrays.
arr = np.arange(8).reshape(-1, 1) * np.ones((1, 4))
arr
array([[0., 0., 0., 0.],
[1., 1., 1., 1.],
[2., 2., 2., 2.],
[3., 3., 3., 3.],
[4., 4., 4., 4.],
[5., 5., 5., 5.],
[6., 6., 6., 6.],
[7., 7., 7., 7.]])
Pass a list/array of row indices:
arr[[4, 3, 0, 6]]
array([[4., 4., 4., 4.],
[3., 3., 3., 3.],
[0., 0., 0., 0.],
[6., 6., 6., 6.]])
Negative indices count from the end:
arr[[-3, -5, -7]]
array([[5., 5., 5., 5.],
[3., 3., 3., 3.],
[1., 1., 1., 1.]])
Pair of index arrays → pick specific tuples (row, col) → 1D result.
arr = np.arange(32).reshape((8, 4))
arr
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23],
[24, 25, 26, 27],
[28, 29, 30, 31]])
arr[[1, 5, 7, 2], [0, 3, 1, 2]]
array([ 4, 23, 29, 10])
Selected (1,0), (5,3), (7,1), (2,2). Arrays must have the same shape.
To get a row-subset × column-subset rectangle, index rows then index columns:
arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
arr[[1, 5, 7, 2], [0, 3, 1, 2]]
array([ 4, 23, 29, 10])
arr[[1, 5, 7, 2], [0, 3, 1, 2]] = 0
arr
array([[ 0, 1, 2, 3],
[ 0, 5, 6, 7],
[ 8, 9, 0, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 0],
[24, 25, 26, 27],
[28, 0, 30, 31]])
Transposing is a reshape that returns a view (no copy).
arr = np.arange(15).reshape((3, 5))
arr
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
arr.T # same as arr.transpose()
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
Common in inner products / Gram matrices.
arr = np.array([[ 0, 1, 0],
[ 1, 2, -2],
[ 6, 3, 2],
[-1, 0, -1],
[ 1, 0, 1]])
np.dot(arr.T, arr) # same as arr.T @ arr
array([[39, 20, 12],
[20, 14, 2],
[12, 2, 10]])
arr.T @ arr
array([[39, 20, 12],
[20, 14, 2],
[12, 2, 10]])
See Appendix A: Advanced NumPy for details on reshape.
swapaxes(axis1, axis2) generalizes transpose; returns a view.
arr
array([[ 0, 1, 0],
[ 1, 2, -2],
[ 6, 3, 2],
[-1, 0, -1],
[ 1, 0, 1]])
arr.swapaxes(0, 1) # swap rows & columns
array([[ 0, 1, 6, -1, 1],
[ 1, 2, 3, 0, 0],
[ 0, -2, 2, -1, 1]])
4×4 from standard normal distribution with np.random.standard_normal:
np.random.standard_normal(size=(4, 4))
array([[-0.2047, 0.4789, -0.5194, -0.5557],
[ 1.9658, 1.3934, 0.0929, 0.2817],
[ 0.769 , 1.2464, 1.0072, -1.2962],
[ 0.275 , 0.2289, 1.3529, 0.8864]])
from random import normalvariate
N = 1_000_000
%timeit samples = [normalvariate(0, 1) for _ in range(N)]
740 ms ± 27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit samples = np.random.standard_normal(N)
32.4 ms ± 96 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
💡 NumPy is >20× faster for this large draw.
rng = np.random.default_rng(seed=12345)
rng.standard_normal((2, 3))
array([[-1.42382504, 1.26372846, -0.87066174],
[-0.25917323, -0.07534331, -0.74088465]])
rng = np.random.default_rng(seed=12345)
rng.standard_normal((2, 3))
array([[-1.42382504, 1.26372846, -0.87066174],
[-0.25917323, -0.07534331, -0.74088465]])
| Method | Description |
|---|---|
| integers(low, high, size) | random integers |
| standard_normal(size) | N(0,1) |
| normal(loc, scale, size) | Gaussian |
| uniform(low, high, size) | continuous uniform |
| binomial(n, p, size) | binomial distribution |
| beta(a, b, size) | beta distribution |
| gamma(shape, scale, size) | gamma distribution |
| chisquare(df, size) | chi-square distribution |
| permutation(x) | return a permuted copy |
| shuffle(x) | permute in place |
rng = np.random.default_rng(0)
rng.integers(0, 10, size=6)
array([5, 6, 9, 2, 6, 7])
rng.uniform(-1, 1, size=(2,3))
array([[ 0.2739239 , 0.4605797 , 0.21999016],
[ 0.21918573, -0.49757134, 0.22181696]])
x = np.arange(5)
rng.permutation(x), x # permutation returns a permuted copy
(array([2, 1, 4, 3, 0]), array([0, 1, 2, 3, 4]))
More methods: NumPy RNG method table
Take a single array argument and return a single array.
arr = np.arange(10)
arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.sqrt(arr)
array([0. , 1. , 1.4142, 1.7321, 2. , 2.2361, 2.4495, 2.6458,
2.8284, 3. ])
np.exp(arr)
array([ 1. , 2.7183, 7.3891, 20.0855, 54.5982, 148.4132,
403.4288, 1096.6332, 2980.958 , 8103.0839])
Take two arrays and return a single array.
rng = np.random.default_rng(12345)
x = rng.standard_normal(8)
y = rng.standard_normal(8)
x, y
(array([-1.3678, 0.6489, 0.3611, -1.9529, 2.3474, 0.9685, -0.7594,
0.9022]),
array([-0.467 , -0.0607, 0.7888, -1.2567, 0.5759, 1.399 , 1.3223,
-0.2997]))
np.maximum(x, y)
array([-0.467 , 0.6489, 0.7888, -1.2567, 2.3474, 1.399 , 1.3223,
0.9022])
arr = rng.standard_normal(7) * 5
arr
array([ 4.5146, -8.1079, -0.7909, 2.2474, -6.718 , -0.4084, 8.6237])
remainder, whole_part = np.modf(arr)
remainder
array([ 0.5146, -0.1079, -0.7909, 0.2474, -0.718 , -0.4084, 0.6237])
whole_part
array([ 4., -8., -0., 2., -6., -0., 8.])
arr
array([ 4.5146, -8.1079, -0.7909, 2.2474, -6.718 , -0.4084, 8.6237])
out = np.zeros_like(arr) # pre-allocate output array
np.add(arr, 1) # allocates a new array
np.add(arr, 1, out=out) # writes into 'out'
out
array([ 5.5146, -7.1079, 0.2091, 3.2474, -5.718 , 0.5916, 9.6237])
| Function | Description |
|---|---|
| abs, fabs | Element-wise absolute value |
| sqrt | Square root (≈ arr ** 0.5) |
| square | Square (≈ arr ** 2) |
| exp | ex element-wise |
| log, log10, log2, log1p | Natural / base-10 / base-2 / log(1+x) |
| sign | Sign of each element (−1, 0, 1) |
| ceil, floor, rint | Round up / down / to nearest (preserves dtype) |
| modf | Fractional and integral parts (returns two arrays) |
| isnan, isfinite, isinf | NaN / finite / infinite checks (Boolean arrays) |
| cos, sin, tan, … | Trig and hyperbolic trig; inverses: arccos, … |
| logical_not | Element-wise NOT (≈ ~arr) |
See NumPy docs for a full, up-to-date list.
| Function | Description |
|---|---|
| add, subtract, multiply | Element-wise arithmetic |
| divide, floor_divide | True division / floor division |
| power | a ** b element-wise |
| maximum, fmax | Element-wise max; fmax ignores NaN |
| minimum, fmin | Element-wise min; fmin ignores NaN |
| mod | Remainder |
| copysign | Copy sign from second arg to first |
| greater, >=, <, <=, ==, != | Comparisons → Boolean arrays |
| logical_and, logical_or, logical_xor | Element-wise logical ops (&, |, ^) |
Matrix multiplication uses @ or np.dot (not a ufunc).
Compute $\sqrt{x^2 + y^2}$ with NumPy (‼️ no loops):
xs = np.array([3, 4, 5, 6])
ys = np.array([4, 5, 12, 8])
hypotenuses = np.sqrt(xs**2 + ys**2)
hypotenuses
array([ 5. , 6.40312424, 13. , 10. ])
xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
cond = np.array([True, False, True, True, False])
np.where(cond, xarr, yarr)
array([1.1, 2.2, 1.3, 1.4, 2.5])
np.where(xarr > 1.3, 2, xarr)
array([1.1, 1.2, 1.3, 2. , 2. ])
arr = np.arange(8).reshape((2, 4))
arr
array([[0, 1, 2, 3],
[4, 5, 6, 7]])
arr.mean()
3.5
arr.sum()
28
arr.mean(axis=1) # across columns
array([1.5, 5.5])
arr.sum(axis=0) # down rows
array([4, 6, 8, 10])
a = np.arange(8)
a
array([0, 1, 2, 3, 4, 5, 6, 7])
a.cumsum()
array([ 0, 1, 3, 6, 10, 15, 21, 28])
b = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
b.cumsum(axis=0) # down rows
array([[ 0, 1, 2],
[ 3, 5, 7],
[ 9, 12, 15]])
b.cumsum(axis=1) # across columns
array([[ 0, 1, 3],
[ 3, 7, 12],
[ 6, 13, 21]])
| Method | Description |
|---|---|
| sum | Total (0 for zero-length) |
| mean | Arithmetic mean (NaN for zero-length) |
| std, var | Std. dev. / variance |
| min, max | Minimum / maximum |
| argmin, argmax | Index of min / max |
| cumsum | Cumulative sum |
| cumprod | Cumulative product |
arr = rng.standard_normal(100)
(arr > 0).sum() # number of positive values
54
(arr <= 0).sum() # number of non-positive values
46
bools = np.array([False, False, True, False])
bools.any()
True
bools.all()
False
a = rng.standard_normal(6)
a
array([-0.95898831, -1.20938829, -1.41229201, 0.54154683, 0.7519394 ,
-0.65876032])
a.sort()
a
array([-1.34360107, -0.08168759, 0.77736134, 0.8286332 , 1.72473993,
2.61815943])
b = rng.standard_normal((3, 4))
b
array([[ 0.43365454, 0.27748366, 0.53025239, 0.53672097],
[ 0.61835001, -0.79501746, 0.30003095, -1.60270159],
[ 0.26679883, -1.26162378, -0.07127081, 0.47404973]])
b.sort(axis=1) # sort each row
b
array([[ 0.27748366, 0.43365454, 0.53025239, 0.53672097],
[-1.60270159, -0.79501746, 0.30003095, 0.61835001],
[-1.26162378, -0.07127081, 0.26679883, 0.47404973]])
Find and sort unique values in an array
names = np.array(["Bob", "Will", "Joe", "Bob", "Will", "Joe", "Joe"])
np.unique(names)
array(['Bob', 'Joe', 'Will'], dtype='<U4')
Find which values in one array are present in another
vals = np.array([6, 0, 0, 3, 2, 5, 6])
np.in1d(vals, [2, 3, 6])
array([ True, False, False, True, True, False, True])
| Function | Description |
|---|---|
| unique(x) | Sorted unique elements |
| intersect1d(x, y) | Sorted common elements |
| union1d(x, y) | Sorted union |
| in1d(x, y) | Membership test (Boolean mask) |
| setdiff1d(x, y) | Elements in x not in y |
| setxor1d(x, y) | Symmetric difference |
Use @ operator (Python 3.5+) or np.dot function.
x = np.array([[1., 2., 3.],
[4., 5., 6.]]) # 2×3
y = np.array([[ 6., 23.],
[-1., 7.],
[ 8., 9.]]) # 3×2
x @ y
array([[ 28., 64.],
[ 67., 181.]])
Matrix–vector products are also done with @ or np.dot.
x @ np.ones(3)
array([ 6., 15.])
from numpy.linalg import inv
X = rng.standard_normal((5, 5))
mat = X.T @ X
inv(mat)
array([[ 3.4993, 2.8444, 3.5956, -16.5538, 4.4733],
[ 2.8444, 2.5667, 2.9002, -13.5774, 3.7678],
[ 3.5956, 2.9002, 4.4823, -18.3453, 4.7066],
[-16.5538, -13.5774, -18.3453, 84.0102, -22.0484],
[ 4.4733, 3.7678, 4.7066, -22.0484, 6.0525]])
mat @ inv(mat)
array([[ 1., 0., 0., 0., 0.],
[ 0., 1., 0., 0., 0.],
[ 0., 0., 1., -0., 0.],
[ 0., 0., 0., 1., 0.],
[-0., 0., 0., 0., 1.]])
| Function | Description |
|---|---|
| dot / @ | Matrix multiplication |
| diag, trace | Diagonal / sum of diagonal |
| det | Determinant |
| eig | Eigenvalues & eigenvectors |
| inv, pinv | Inverse / pseudoinverse |
| qr, svd | QR / singular value decomposition |
| solve, lstsq | Solve Ax=b (square / least squares) |