Python Essentials for AI Engineers · Lesson 27 of 36
NumPy Arrays vs Python Lists
Why NumPy?
Python lists are flexible but slow for numerical computation. NumPy arrays are fast because:
- Homogeneous dtype — all elements the same type, stored contiguously in memory
- C implementation — operations run in C, not Python
- Vectorized operations — apply functions to entire arrays without Python loops
import numpy as np
# Python list: heterogeneous, pointer-based, slow loops
py_list = [1.0, 2.0, 3.0, 4.0, 5.0]
# NumPy array: typed, contiguous memory, fast
np_array = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
# Speed comparison: multiply every element by 2
import timeit
data_list = list(range(1_000_000))
data_np = np.arange(1_000_000, dtype=np.float64)
list_time = timeit.timeit(lambda: [x * 2 for x in data_list], number=100)
np_time = timeit.timeit(lambda: data_np * 2, number=100)
print(f"List loop: {list_time:.3f}s")
print(f"NumPy: {np_time:.3f}s")
# NumPy is typically 50-100x faster for large arraysCreating Arrays
import numpy as np
# From Python list
arr = np.array([1, 2, 3, 4, 5])
print(arr.dtype) # int64 (platform-dependent)
# Specify dtype
arr_f32 = np.array([1.0, 2.0, 3.0], dtype=np.float32)
arr_f64 = np.array([1.0, 2.0, 3.0], dtype=np.float64) # Default for floats
# 2D array (matrix)
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(matrix.shape) # (3, 3)
# Utility constructors
zeros = np.zeros((3, 4)) # 3×4 array of 0.0
ones = np.ones((2, 5)) # 2×5 array of 1.0
eye = np.eye(3) # 3×3 identity matrix
rng = np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
lin = np.linspace(0, 1, 5) # [0, 0.25, 0.5, 0.75, 1.0] — 5 evenly spaced points
rand = np.random.rand(3, 4) # Random 3×4 matrix, uniform [0, 1)
randn = np.random.randn(3, 4) # Random 3×4 matrix, standard normalShape, dtype, and ndim
embeddings = np.random.randn(100, 1536) # 100 embeddings, 1536 dimensions each
print(embeddings.shape) # (100, 1536)
print(embeddings.ndim) # 2
print(embeddings.dtype) # float64
print(embeddings.size) # 153600 (total elements)
print(embeddings.nbytes) # 1228800 (bytes = 153600 * 8 bytes/float64)
# Reshape: same data, different shape
reshaped = embeddings.reshape(1, 100, 1536) # Add batch dimension
print(reshaped.shape) # (1, 100, 1536)
flat = embeddings.flatten() # 1D copy
flat = embeddings.ravel() # 1D view (no copy if possible)
print(flat.shape) # (153600,)Dtype: Choosing the Right One
# Memory and precision tradeoffs:
# float64 (8 bytes): default, full precision
# float32 (4 bytes): used in most ML frameworks (PyTorch default)
# float16 (2 bytes): half precision for GPU inference
# int32 (4 bytes): token IDs, class labels
embeddings_f64 = np.random.randn(1000, 1536).astype(np.float64)
embeddings_f32 = embeddings_f64.astype(np.float32)
print(embeddings_f64.nbytes) # 12,288,000 bytes (~12 MB)
print(embeddings_f32.nbytes) # 6,144,000 bytes (~6 MB)
# PyTorch models expect float32 by default
# OpenAI embeddings return float64 by default — convert when neededVectorized Operations
Operations on NumPy arrays apply element-wise automatically:
a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])
# Element-wise arithmetic (no loop needed)
print(a + b) # [5. 7. 9.]
print(a * b) # [4. 10. 18.]
print(a ** 2) # [1. 4. 9.]
print(a / b) # [0.25 0.4 0.5]
# Universal functions (ufuncs) — apply to every element
print(np.sqrt(a)) # [1. 1.414 1.732]
print(np.exp(a)) # [2.718 7.389 20.086]
print(np.log(a)) # [0. 0.693 1.099]
# Key AI computation: cosine similarity
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
query_embedding = np.random.randn(1536)
doc_embedding = np.random.randn(1536)
sim = cosine_similarity(query_embedding, doc_embedding)
print(f"Cosine similarity: {sim:.4f}")
# Batch cosine similarity: query vs all documents at once
def batch_cosine_similarity(query: np.ndarray, docs: np.ndarray) -> np.ndarray:
"""Compute similarity between one query and many documents."""
query_norm = query / np.linalg.norm(query) # Normalize query
doc_norms = docs / np.linalg.norm(docs, axis=1, keepdims=True) # Normalize each doc
return doc_norms @ query_norm # Matrix multiply
query = np.random.randn(1536)
doc_embeddings = np.random.randn(100, 1536) # 100 documents
similarities = batch_cosine_similarity(query, doc_embeddings)
print(similarities.shape) # (100,) — one score per document
top_indices = np.argsort(similarities)[::-1][:5] # Top 5 document indicesAggregation
arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Global aggregations
print(arr.sum()) # 45
print(arr.mean()) # 5.0
print(arr.max()) # 9
print(arr.min()) # 1
print(arr.std()) # Standard deviation
# Axis-wise (axis=0: collapse rows, result per column; axis=1: collapse columns, result per row)
print(arr.sum(axis=0)) # [12 15 18] — sum each column
print(arr.sum(axis=1)) # [6 15 24] — sum each row
print(arr.max(axis=1)) # [3 6 9] — max of each row
# Useful in ML
retrieval_scores = np.array([[0.92, 0.45, 0.78], [0.61, 0.89, 0.33]])
print(np.mean(retrieval_scores, axis=1)) # Mean score per query
print(np.argmax(retrieval_scores, axis=1)) # Index of highest-scoring doc per queryViews vs Copies
arr = np.arange(10)
# Slicing returns a VIEW — same data in memory
view = arr[2:5]
view[0] = 99
print(arr) # [ 0 1 99 3 4 5 6 7 8 9] — original changed!
# .copy() returns an independent copy
copy = arr[2:5].copy()
copy[0] = 0
print(arr) # Unchanged
# Check if array is a view
print(view.base is arr) # True — view shares arr's data
print(copy.base is None) # True — copy owns its data
# Why it matters in AI: avoid unintended mutation
embeddings = load_embeddings()
batch = embeddings[:32] # View — modifications affect original
batch = embeddings[:32].copy() # Safe independent copyNumPy vs Python List: Summary
| Feature | Python List | NumPy Array |
|---|---|---|
| Type | Heterogeneous (any type) | Homogeneous (one dtype) |
| Memory | Pointers to objects | Contiguous typed buffer |
| Element-wise ops | Manual loop | Vectorized (C speed) |
| Indexing | Supports negative | Supports negative + fancy |
| Speed (numeric ops) | ~1x | ~50-100x |
| Memory (floats) | ~56 bytes/element | 4-8 bytes/element |
| Multidimensional | Nested lists | Native ndarray |
| Math functions | math module (scalar) | np. (vectorized) |
| When to use | Mixed types, small data | Numeric computation, ML |