Free GuideBeginner
50 min read
#NumPy#ndarrays#Vectorization#Broadcasting#Linear Algebra#Memory Layout

01. NumPy Fundamentals & Vector Math

Core scientific computing with NumPy: N-dimensional array memory layouts (strides, C vs Fortran order), vectorized arithmetic, broadcasting rules, advanced indexing, and linear algebra routines.

Notebook 1: NumPy Fundamentals and Vector Math

Module: Data Analytics Level: Beginner to Intermediate Prerequisites: Notebook 0 (Introduction to Data Analytics) Estimated Time: 3–4 hours


Table of Contents#

  1. Why NumPy Exists
  2. The ndarray: NumPy's Core Data Structure
  3. Creating Arrays
  4. Array Indexing and Slicing
  5. Vectorized Operations
  6. Broadcasting
  7. Aggregation Functions
  8. Vector Math: Dot Products and Norms
  9. Matrix Operations
  10. Performance: Why Vectorization Wins
  11. Hands-On: A Complete Vectorized Analysis
  12. Case Studies
  13. Common Misconceptions and Pitfalls
  14. Glossary
  15. Exercises
  16. Quiz
  17. Summary
  18. Further Reading

Learning Objectives#

By the end of this notebook, you will be able to:

  • Explain why NumPy arrays are dramatically faster than native Python lists for numeric computation
  • Create, index, and slice NumPy arrays of one and multiple dimensions
  • Apply vectorized operations in place of explicit loops
  • Explain and apply NumPy's broadcasting rules to operate on arrays of different shapes
  • Compute vector norms, dot products, and basic matrix operations
  • Benchmark and quantify the performance difference between loop-based and vectorized code
  • Perform a complete small-scale numeric analysis using only vectorized NumPy operations

1. Why NumPy Exists#

Python's built-in list is a flexible, general-purpose container, but this flexibility comes at a cost for numeric computation. Each element of a Python list is a full Python object, individually allocated in memory and individually type-checked at runtime. NumPy (Numerical Python) instead provides the ndarray (n-dimensional array), a data structure that stores homogeneous numeric data in a single contiguous block of memory, using fixed, low-level types (like 64-bit floats) rather than generic Python objects.

This design choice, borrowed from lower-level languages like C, is what allows NumPy to perform mathematical operations on entire arrays through vectorization: pushing the actual loop down into pre-compiled C code, rather than looping at the (much slower) Python interpreter level. This single idea, previewed briefly in Notebook 0, Section 6.3, is the foundation of nearly every performance-sensitive technique in this course, and Section 10 quantifies exactly how large the resulting speedup is.


2. The ndarray: NumPy's Core Data Structure#

2.1 Key Properties#

Every NumPy array has three defining properties:

  • shape: a tuple describing the size along each dimension (e.g., (1000,) for a 1D array of 1000 elements, (100, 5) for a 100-row, 5-column 2D array)
  • dtype: the data type of every element in the array (e.g., int64, float64, bool) — critically, all elements share the same dtype, unlike a Python list, which can mix types freely
  • ndim: the number of dimensions (axes) of the array
Python
import numpy as np arr = np.array([1, 2, 3, 4, 5]) print(f"Shape: {arr.shape}") print(f"Dtype: {arr.dtype}") print(f"Number of dimensions: {arr.ndim}")

Expected output:

code
Shape: (5,) Dtype: int64 Number of dimensions: 1

2.2 Why Fixed Dtypes Matter#

Because every element shares a single dtype, NumPy knows in advance exactly how many bytes each element occupies and can lay them out contiguously in memory, enabling both fast element access and fast vectorized operations (Section 5). Storing a mix of an integer, a string, and a float in a single NumPy array forces NumPy to fall back to a generic, slow object dtype, which loses essentially all of the performance benefit; this is one reason keeping data appropriately typed matters not just for correctness (Notebook 4 covers this from the cleaning perspective) but for performance.


3. Creating Arrays#

3.1 From Existing Python Data#

Python
import numpy as np # From a list a = np.array([1, 2, 3]) # From a nested list, producing a 2D array b = np.array([[1, 2, 3], [4, 5, 6]]) print(f"b shape: {b.shape}") # (2, 3): 2 rows, 3 columns

3.2 Built-in Generation Functions#

Python
zeros = np.zeros((3, 4)) # 3x4 array of zeros ones = np.ones((2, 2)) # 2x2 array of ones identity = np.eye(3) # 3x3 identity matrix range_arr = np.arange(0, 10, 2) # [0, 2, 4, 6, 8] - like Python's range, but returns an array linspace_arr = np.linspace(0, 1, 5) # [0, 0.25, 0.5, 0.75, 1.0] - 5 evenly spaced points random_arr = np.random.default_rng(42).normal(size=(3, 3)) # random normal values

Expected output for range_arr and linspace_arr:

›range_arr: [0 2 4 6 8] linspace_arr: [0. 0.25 0.5 0.75 1. ]

arange is specified by a step size and can produce a variable number of elements depending on the range; linspace is specified by a fixed number of elements and computes the step size automatically. This distinction matters when you need an exact count of points (use linspace) versus an exact spacing (use arange).


4. Array Indexing and Slicing#

4.1 Basic Indexing#

NumPy indexing extends Python's list indexing and slicing syntax to multiple dimensions, using a single comma-separated tuple of indices rather than chained brackets:

Python
arr = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]]) print(arr[0, 0]) # 10 - first row, first column print(arr[1, :]) # [40 50 60] - entire second row print(arr[:, 2]) # [30 60 90] - entire third column print(arr[0:2, 1:3]) # [[20 30] [50 60]] - a sub-matrix slice

4.2 Boolean Masking#

One of the most important indexing patterns in practice: using a boolean array (or condition) to select elements, which is the array equivalent of a filtering operation.

Python
data = np.array([12, 45, 7, 89, 34, 3, 67]) mask = data > 30 print(mask) # [False True False True True False True] print(data[mask]) # [45 89 34 67] - only elements greater than 30 # Often written as a single expression: print(data[data > 30]) # same result

This pattern, applying a condition directly to an array to select a subset, is used constantly throughout data cleaning (Notebook 4) and exploratory analysis (Notebook 7), and is dramatically faster than an equivalent Python-level loop with an if statement, for the same reasons discussed in Section 10.

4.3 Fancy Indexing#

Arrays can also be indexed with a list or array of specific integer positions:

Python
data = np.array([10, 20, 30, 40, 50]) indices = [0, 2, 4] print(data[indices]) # [10 30 50]

4.4 Views vs. Copies#

A critical and frequently surprising behavior: basic slicing (using :) returns a view into the original array, not a new copy. Modifying a view modifies the original data.

Python
original = np.array([1, 2, 3, 4, 5]) view = original[1:4] view[0] = 999 print(original) # [1 999 3 4 5] - the original was changed! # To avoid this, explicitly create a copy: safe_copy = original[1:4].copy() safe_copy[0] = -1 print(original) # unaffected: [1 999 3 4 5]

Boolean masking and fancy indexing (Sections 4.2, 4.3), by contrast, always return a copy, not a view. This distinction between slicing (view) and masking/fancy indexing (copy) is a common source of subtle bugs and is worth committing to memory.


5. Vectorized Operations#

5.1 Element-Wise Arithmetic#

Standard arithmetic operators, applied directly to arrays, operate element-wise without any explicit loop:

Python
a = np.array([1, 2, 3, 4]) b = np.array([10, 20, 30, 40]) print(a + b) # [11 22 33 44] print(a * b) # [10 40 90 160] print(b / a) # [10. 10. 10. 10.] print(a ** 2) # [1 4 9 16]

5.2 Universal Functions (ufuncs)#

NumPy provides vectorized versions of common mathematical functions, called universal functions, that also operate element-wise across an entire array in a single call:

Python
angles = np.array([0, np.pi/2, np.pi]) print(np.sin(angles)) # [0.0000000e+00 1.0000000e+00 1.2246468e-16] print(np.sqrt([4, 9, 16])) # [2. 3. 4.] print(np.exp([0, 1, 2])) # [1. 2.71828183 7.3890561 ] print(np.log([1, np.e, np.e**2])) # [0. 1. 2.]

5.3 Comparison and Logical Operations#

Comparisons also vectorize automatically, producing boolean arrays (this is precisely what powers the masking pattern from Section 4.2):

Python
a = np.array([1, 5, 3, 8, 2]) print(a > 3) # [False True False True False] print((a > 2) & (a < 8)) # [False True True False False] # Note: use & and | with parentheses around each condition for # element-wise array logic, NOT Python's `and`/`or`, which do not # work correctly on arrays.

6. Broadcasting#

6.1 The Problem Broadcasting Solves#

Element-wise operations, as shown in Section 5.1, normally require both arrays to have the exact same shape. Broadcasting is the set of rules NumPy uses to perform element-wise operations on arrays of different but compatible shapes, without you needing to manually duplicate data to make the shapes match.

6.2 The Broadcasting Rules#

Two dimensions are compatible for broadcasting when they are equal, or when one of them is 1. NumPy compares shapes starting from the trailing (rightmost) dimension and works backward:

code
Shape A: (3, 4) Shape B: (4,) Result: (3, 4) -- B is treated as if it had shape (1, 4), then stretched to (3, 4) without actually copying data

6.3 Scalar Broadcasting#

The simplest and most common case: an operation between an array and a single scalar value, where the scalar is conceptually "stretched" to match the array's shape.

Python
prices = np.array([19.99, 24.99, 9.99, 49.99]) discounted = prices * 0.9 # scalar 0.9 is broadcast across every element print(discounted)

Expected output:

[17.991 22.491  8.991 44.991]

6.4 Row and Column Broadcasting#

A more powerful case: combining a 2D array with a 1D array to apply an operation across every row (or column) at once.

Python
# A matrix of monthly sales for 3 products across 4 months sales = np.array([ [100, 120, 90, 130], [200, 210, 195, 220], [50, 55, 48, 60] ]) # Subtract each product's own average from every value in its row # (mean of each row, computed along axis=1, keeping dimensions for broadcasting) row_means = sales.mean(axis=1, keepdims=True) print(f"Row means shape: {row_means.shape}") # (3, 1) centered = sales - row_means print(centered)

Expected output:

code
Row means shape: (3, 1) [[ -10. 10. -20. 20. ] [-11.25 -1.25 -16.25 8.75] [ -3.25 1.75 -5.25 6.75]]

Here, sales has shape (3, 4) and row_means has shape (3, 1). Per the broadcasting rules, the trailing dimensions (4 versus 1) are compatible because one of them is 1, so row_means is conceptually stretched across all 4 columns for each row, without NumPy ever actually allocating a full (3, 4) copy of the means. This exact pattern, centering each row by its own mean, reappears constantly in statistics (standardization, Notebook 6) and preprocessing.

6.5 When Broadcasting Fails#

Python
a = np.array([1, 2, 3]) # shape (3,) b = np.array([1, 2, 3, 4]) # shape (4,) result = a + b # raises ValueError: operands could not be broadcast together

Expected output:

ValueError: operands could not be broadcast together with shapes (3,) (4,)

Neither trailing dimension (3 versus 4) is equal, and neither is 1, so no valid broadcasting exists; this is a common source of shape-mismatch errors in real analysis code, and reading the resulting error message's shapes carefully is the fastest way to diagnose it.


7. Aggregation Functions#

7.1 Common Aggregations#

Python
data = np.array([[4, 9, 2], [7, 1, 8], [3, 6, 5]]) print(f"Sum (all elements): {data.sum()}") print(f"Mean (all elements): {data.mean():.2f}") print(f"Max (all elements): {data.max()}") print(f"Standard deviation: {data.std():.2f}")

Expected output:

code
Sum (all elements): 45 Mean (all elements): 5.00 Max (all elements): 9 Standard deviation: 2.58

7.2 Axis-Wise Aggregation#

The axis parameter controls which dimension is collapsed by the aggregation. This is one of the most important and initially confusing concepts in NumPy: axis=0 collapses rows (aggregating down each column), and axis=1 collapses columns (aggregating across each row).

Python
print(f"Column sums (axis=0): {data.sum(axis=0)}") # sum down each column print(f"Row sums (axis=1): {data.sum(axis=1)}") # sum across each row

Expected output:

›Column sums (axis=0): [14 16 15] Row sums (axis=1): [15 16 14]

A useful mnemonic: axis=0 moves down the rows (producing one result per column), and axis=1 moves across the columns (producing one result per row). This exact same axis convention is reused identically in Pandas (Notebook 2), so mastering it here pays off immediately in the next notebook.


8. Vector Math: Dot Products and Norms#

8.1 The Dot Product#

The dot product of two vectors a=(a1,…,an)\mathbf{a} = (a_1, \dots, a_n) and b=(b1,…,bn)\mathbf{b} = (b_1, \dots, b_n) is defined as:

a⋅b=∑i=1naibi\mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^{n} a_i b_i

Python
a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) dot_product = np.dot(a, b) # equivalently: a @ b print(f"Dot product: {dot_product}") # 1*4 + 2*5 + 3*6 = 32

The dot product has a direct analytics interpretation: if a represents item quantities purchased and b represents item prices, a @ b computes the total purchase amount in a single vectorized operation, avoiding an explicit loop over items.

Python
quantities = np.array([3, 1, 2, 5]) prices = np.array([9.99, 24.99, 14.99, 4.99]) total_cost = quantities @ prices print(f"Total cost: ${total_cost:.2f}")

Expected output:

Total cost: $89.90

8.2 Vector Norms#

The L2 norm (Euclidean norm) of a vector measures its length or magnitude:

∥x∥2=∑i=1nxi2\|\mathbf{x}\|_2 = \sqrt{\sum_{i=1}^{n} x_i^2}

Python
x = np.array([3, 4]) norm = np.linalg.norm(x) print(f"L2 norm: {norm}") # sqrt(3^2 + 4^2) = sqrt(25) = 5.0

The L1 norm (Manhattan norm) instead sums absolute values:

∥x∥1=∑i=1n∣xi∣\|\mathbf{x}\|_1 = \sum_{i=1}^{n} |x_i|

Python
l1_norm = np.linalg.norm(x, ord=1) print(f"L1 norm: {l1_norm}") # |3| + |4| = 7.0

These norms directly reappear as the mathematical basis of Ridge (L2) and Lasso (L1) regularization penalties in the Machine Learning course, and as the basis of distance metrics used in k-Nearest Neighbors and clustering algorithms later in that same course.

8.3 Cosine Similarity#

A closely related and extremely common analytics quantity, cosine similarity measures the angle between two vectors, independent of their magnitude:

cosine_similarity(a,b)=a⋅b∥a∥2∥b∥2\text{cosine\_similarity}(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\|_2 \|\mathbf{b}\|_2}

Python
def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) user_a_ratings = np.array([5, 3, 0, 1]) user_b_ratings = np.array([4, 0, 0, 1]) similarity = cosine_similarity(user_a_ratings, user_b_ratings) print(f"Cosine similarity: {similarity:.3f}")

Expected output:

Cosine similarity: 0.789

This quantity, ranging from -1 (opposite direction) to 1 (identical direction), is widely used in analytics and recommendation contexts to measure similarity between two numeric profiles (e.g., two users' rating patterns) without being distorted by one profile simply having larger raw numbers than the other.


9. Matrix Operations#

9.1 Matrix Multiplication#

For two matrices AA (shape m×km \times k) and BB (shape k×pk \times p), matrix multiplication produces a result of shape m×pm \times p, where each entry is a dot product of a row of AA and a column of BB:

(AB)ij=∑l=1kAilBlj(AB)_{ij} = \sum_{l=1}^{k} A_{il} B_{lj}

Python
A = np.array([[1, 2], [3, 4]]) B = np.array([[5, 6], [7, 8]]) result = A @ B # matrix multiplication, NOT element-wise print(result)

Expected output:

›[[19 22] [43 50]]

Note carefully: A @ B (matrix multiplication) is different from A * B (element-wise multiplication). Confusing these two operators is one of the most common NumPy mistakes among beginners, and produces a result with no error message, just silently wrong numbers, since both operations are individually valid but compute entirely different things.

Python
print(A * B) # element-wise, NOT matrix multiplication

Expected output:

›[[ 5 12] [21 32]]

9.2 Transpose#

The transpose of a matrix flips it over its diagonal, swapping rows and columns:

Python
M = np.array([[1, 2, 3], [4, 5, 6]]) print(f"Original shape: {M.shape}") # (2, 3) print(f"Transposed shape: {M.T.shape}") # (3, 2) print(M.T)

Expected output:

code
Original shape: (2, 3) Transposed shape: (3, 2) [[1 4] [2 5] [3 6]]

9.3 Why This Matters for Analytics#

Matrix operations are the computational backbone of many analytics techniques covered later in this course: computing a full correlation matrix (Notebook 9) between many variables at once is fundamentally a matrix operation, and standardizing many columns simultaneously (Notebook 6) relies on the row/column broadcasting introduced in Section 6.4.


10. Performance: Why Vectorization Wins#

10.1 A Direct Benchmark#

This example directly measures the performance gap previewed conceptually in Notebook 0, Section 6.3 and the case study in Notebook 0, Section 9.3.

Python
import numpy as np import time n = 1_000_000 data = list(range(n)) data_arr = np.arange(n) # Loop-based approach start = time.perf_counter() result_loop = [x * 2 for x in data] loop_time = time.perf_counter() - start # Vectorized approach start = time.perf_counter() result_vectorized = data_arr * 2 vectorized_time = time.perf_counter() - start print(f"Loop-based time: {loop_time:.4f} seconds") print(f"Vectorized time: {vectorized_time:.4f} seconds") print(f"Speedup: {loop_time / vectorized_time:.1f}x")

Expected output (approximate, will vary by hardware):

code
Loop-based time: 0.0612 seconds Vectorized time: 0.0018 seconds Speedup: 34.0x

10.2 Why This Gap Exists#

Three compounding factors explain the speedup:

  1. No per-element type checking: a Python list element can be any type, so every arithmetic operation on it must check its type at runtime; a NumPy array's fixed dtype (Section 2.2) eliminates this check entirely.
  2. No per-element Python object overhead: each Python integer is a full object with reference counting and other bookkeeping; a NumPy array stores raw numeric values contiguously, with none of that overhead per element.
  3. Compiled, optimized loops: NumPy's internal operations are implemented in C and often use hardware-level optimizations (such as SIMD instructions that process multiple values in a single CPU instruction), none of which the Python interpreter's own loop can access directly.

10.3 When Vectorization Does Not Help (Or Is Not Possible)#

Not every operation vectorizes cleanly. Operations with complex, data-dependent branching logic, or that inherently require sequential processing (where each step depends on the previous result in a way that cannot be expressed as a single array operation), may not benefit from or may not be expressible via vectorization at all. In these cases, tools outside NumPy's core vectorization model, such as numba (just-in-time compilation) or Cython, exist for extreme cases, though they are outside the scope of this course. The overwhelming majority of standard analytics operations (arithmetic, filtering, aggregation, reshaping), however, vectorize cleanly and should default to doing so.


11. Hands-On: A Complete Vectorized Analysis#

This example computes, entirely without explicit loops, a z-score standardization and outlier flag for a dataset of transaction amounts, directly combining aggregation (Section 7), broadcasting (Section 6), and vectorized comparisons (Section 5.3).

Python
import numpy as np rng = np.random.default_rng(42) transaction_amounts = rng.normal(loc=85, scale=25, size=1000) # Inject a few artificial outliers transaction_amounts[500] = 950 transaction_amounts[750] = -200 # Step 1: compute mean and standard deviation (vectorized aggregation) mean = transaction_amounts.mean() std = transaction_amounts.std() # Step 2: compute z-scores for every transaction at once (vectorized, broadcasted) z_scores = (transaction_amounts - mean) / std # Step 3: flag outliers where |z-score| > 3 (vectorized boolean comparison) is_outlier = np.abs(z_scores) > 3 print(f"Mean: {mean:.2f}, Std: {std:.2f}") print(f"Number of outliers detected: {is_outlier.sum()}") print(f"Outlier values: {transaction_amounts[is_outlier]}")

Expected output (approximate):

code
Mean: 85.87, Std: 39.31 Number of outliers detected: 2 Outlier values: [950. -200.]

Every step in this analysis, computing the mean and standard deviation, subtracting and dividing across the entire array, and flagging entries above a threshold, executes as a single vectorized operation over all 1,000 values, with no explicit Python-level loop anywhere in the code. This exact z-score outlier detection technique is revisited formally in Notebook 4 (Data Cleaning and Preparation).


12. Case Studies#

12.1 Case Study: A Recommendation Feature Built on Cosine Similarity#

An e-commerce team wants a simple "customers who rated similarly to you" feature. Rather than building a full machine learning model, an analyst represents each customer as a vector of ratings across a shared set of products and computes pairwise cosine similarity (Section 8.3) between customers, recommending products liked by the most similar customers.

Analysis: This is a clean illustration of vector math providing direct analytical value without requiring a trained predictive model at all. The choice of cosine similarity, specifically, matters here: it deliberately ignores the raw magnitude of ratings (a customer who rates everything a 5 and a customer who rates everything a 4 but with the same relative pattern would be judged highly similar), focusing purely on the pattern of relative preference, which is usually what similarity should capture in this context.

Lesson: Understanding the mathematical meaning of a vector operation, not just its syntax, is what allows an analyst to choose cosine similarity over, say, Euclidean distance (which would penalize the two customers above for their different rating scales, even though their relative preferences are identical).

12.2 Case Study: A Silent Bug From Confusing * and @#

A financial analyst computing portfolio returns writes weights * returns intending matrix multiplication to compute a single weighted total return, but weights and returns are both 2D arrays and the operation is actually element-wise (Section 9.1), silently producing a full matrix of individual weighted contributions rather than the intended single aggregated number. The error is not caught for several weeks because the code runs without any error message and produces numbers that look superficially plausible.

Diagnosis: This is a direct real-world instance of the exact pitfall described in Section 9.1: * and @ are both syntactically valid between two same-shaped 2D arrays, so no error is raised, but they compute mathematically different things, and only careful inspection of output shapes (or a test against a known, manually calculated result) revealed the mistake.

Lesson: When performing matrix operations, always verify the resulting shape matches the shape you mathematically expect (result.shape) as a first-line sanity check, since NumPy will not warn you about a valid-but-wrong operator choice.

12.3 Case Study: A Nightly Batch Job That Missed Broadcasting#

An analytics team needed to subtract each region's own average revenue from every observation within that region, exactly as in Section 6.4's row-centering example. An initial implementation using a Python loop over regions, computing each region's mean and subtracting it with nested indexing, worked correctly but ran for over 40 minutes nightly on a dataset with several million rows across thousands of regions. Rewriting the operation using groupby combined with vectorized broadcasting (previewed here, covered fully in Notebook 3) reduced runtime to under 90 seconds.

Diagnosis: The original implementation paid Python-level loop overhead once per region and, within each region, again per row, compounding the cost described in Section 10.2 across two nested loop levels. The vectorized rewrite pushed both the per-region aggregation and the subtraction into compiled operations operating on the whole dataset at once.

Lesson: This case study is a larger-scale, more structurally realistic version of Section 10.1's benchmark and Notebook 0's case study (Section 9.3), and directly previews why Notebook 3 dedicates significant attention to groupby as a high-performance alternative to explicit iteration over groups.


13. Common Misconceptions and Pitfalls#

MisconceptionReality
"NumPy arrays and Python lists are basically interchangeable, just with extra features."Their underlying memory layout and typing model are fundamentally different, per Section 2, which is the entire source of NumPy's performance advantage.
"Slicing an array always gives you a safe, independent copy."Basic slicing returns a view that shares memory with the original array, per Section 4.4; modifying a slice can silently modify the source data.
"* between two matrices always performs matrix multiplication."* performs element-wise multiplication; @ (or np.dot / np.matmul) performs matrix multiplication, per Section 9.1 and the case study in Section 12.2.
"axis=0 and axis=1 are arbitrary and easy to guess correctly without checking."The convention (axis=0 collapses down columns, axis=1 collapses across rows, per Section 7.2) is consistent but frequently misremembered; always sanity-check the output shape.
"Vectorization always requires rewriting an algorithm from scratch."Many common patterns (arithmetic, filtering, aggregation) vectorize directly via built-in NumPy functions and operators, without redesigning the underlying logic, as shown throughout Sections 5 through 9.
"Broadcasting can combine arrays of any two shapes."Broadcasting only works when, comparing shapes from the trailing dimension, every pair of dimensions is either equal or one of them is 1, per Section 6.2; otherwise it raises an explicit error, as in Section 6.5.

14. Glossary#

TermDefinition
ndarrayNumPy's core n-dimensional array data structure, storing homogeneous, fixed-type data contiguously in memory
dtypeThe fixed data type shared by every element in a NumPy array
VectorizationPerforming an operation on an entire array at once via compiled code, rather than looping element by element in Python
BroadcastingNumPy's rules for performing element-wise operations on arrays of different but compatible shapes
ViewAn array that shares underlying memory with another array, such that modifying one can modify the other
AxisA specific dimension of a multi-dimensional array along which an operation (such as an aggregation) is applied
Dot productThe sum of element-wise products of two vectors, ∑aibi\sum a_ib_i
L2 norm (Euclidean norm)The square root of the sum of squared vector elements, representing vector length
L1 norm (Manhattan norm)The sum of absolute values of vector elements
Cosine similarityA measure of the angle between two vectors, independent of their magnitude
Matrix multiplicationThe operation combining two matrices via row-by-column dot products, denoted @ in NumPy, distinct from element-wise *

15. Exercises#

Exercise 1 (Conceptual)#

Explain, using the concepts from Section 2.2 and Section 10.2, why a NumPy array containing a mix of integers and strings (forced into the generic object dtype) loses most of the performance benefit of using NumPy at all.

<details> <summary>Solution</summary>

When an array is forced into the generic object dtype, each element is stored as a reference to a full Python object rather than as a raw, fixed-size numeric value in contiguous memory (Section 2.2). This means arithmetic operations on such an array must fall back to per-element Python-level processing, including type checking and object overhead, exactly the costs that Section 10.2 identifies as being eliminated by proper fixed-dtype vectorization. In effect, an object-dtype array behaves computationally much more like a Python list than like a true NumPy numeric array, even though it is technically still an ndarray.

</details>

Exercise 2 (Conceptual)#

Given a 2D array data with shape (500, 10) representing 500 observations of 10 features, and a 1D array feature_means with shape (10,), explain why data - feature_means works correctly via broadcasting, and what shape feature_means would need to have failed to broadcast against data correctly.

<details> <summary>Solution</summary>

Comparing shapes from the trailing dimension (Section 6.2): data has shape (500, 10) and feature_means has shape (10,), which NumPy treats as (1, 10) for broadcasting purposes. The trailing dimensions (10 and 10) match exactly, and the next dimension (500 versus the implicit 1) is compatible because one of them is 1, so feature_means is broadcast down each of the 500 rows, subtracting each feature's mean from every observation's corresponding feature value, correctly centering each column. A shape that would fail to broadcast, for example (500,), would not align on the trailing dimension (500 versus 10, neither equal nor equal to 1), producing the kind of explicit shape-mismatch error shown in Section 6.5.

</details>

Exercise 3 (Coding)#

Given two vectors representing a customer's spending across 5 product categories and the average spending across all customers in those same 5 categories, compute the Euclidean distance (using np.linalg.norm on the difference of the two vectors) and the cosine similarity between them. Explain, in one sentence, a scenario where these two metrics would give meaningfully different impressions of "how similar" the customer is to the average.

<details> <summary>Solution</summary>
Python
import numpy as np customer_spending = np.array([120, 45, 0, 300, 15]) average_spending = np.array([60, 22, 5, 150, 8]) euclidean_distance = np.linalg.norm(customer_spending - average_spending) cosine_sim = np.dot(customer_spending, average_spending) / ( np.linalg.norm(customer_spending) * np.linalg.norm(average_spending) ) print(f"Euclidean distance: {euclidean_distance:.2f}") print(f"Cosine similarity: {cosine_sim:.3f}")

A customer who spends exactly twice the average in every single category would have a very high (large) Euclidean distance from the average, since every raw difference is large, but a cosine similarity of essentially 1.0, since the pattern (relative proportion across categories) is identical to the average, directly reflecting the distinction discussed in Section 8.3 and the recommendation case study in Section 12.1: Euclidean distance is sensitive to overall magnitude, while cosine similarity is not.

</details>

Exercise 4 (Applied Thinking)#

A colleague writes a nightly script that loops over each row of a 2-million-row NumPy array to compute a running discount calculation, and it currently takes 25 minutes to run. Using the concepts from Sections 10 and 12.3, describe two concrete steps you would investigate first to speed this up, without seeing the actual code.

<details> <summary>Solution</summary>

Two natural first investigations: (1) Check whether the per-row calculation can be expressed as a vectorized operation over the entire array at once (Section 5, Section 10.1) — most simple arithmetic-style calculations (like a discount applied based on a threshold condition) can be rewritten using boolean masking (Section 4.2) and broadcasting (Section 6) instead of an explicit loop, which alone often accounts for an order-of-magnitude or greater speedup, as quantified in Section 10.1's benchmark. (2) Check whether the current dtype is a generic object dtype rather than a fixed numeric dtype like float64 (Section 2.2, Exercise 1) — if the array was built by mixing types at some point, even a rewritten vectorized version would fail to achieve full performance until the dtype is corrected, since compiled vectorized operations depend on a fixed, known element size and type.

</details>

16. Quiz#

Q1. What is the primary reason NumPy arrays outperform Python lists for numeric computation? a) NumPy arrays can only store integers b) NumPy arrays store homogeneous, fixed-type data in contiguous memory, enabling compiled, low-overhead operations c) Python lists are deprecated d) NumPy arrays automatically run on a GPU

Q2. What does basic slicing (e.g., arr[1:4]) return? a) Always an independent copy of the data b) A view sharing memory with the original array c) A new array with a different dtype d) An error, since slicing is not supported

Q3. What is the key difference between A * B and A @ B for two 2D NumPy arrays? a) There is no difference; they are aliases for the same operation b) * performs element-wise multiplication; @ performs matrix multiplication c) * only works on 1D arrays d) @ is deprecated in favor of *

Q4. For a 2D array with shape (100, 5), what does axis=0 refer to in an aggregation like .sum(axis=0)? a) Summing across each row, producing 100 results b) Summing down each column, producing 5 results c) Summing the entire array into a single number d) An invalid axis for this array's shape

Q5. True or False: Cosine similarity between two vectors is affected by the overall magnitude (scale) of the vectors, not just their direction.

<details> <summary>Answers</summary>
  1. b) NumPy arrays store homogeneous, fixed-type data in contiguous memory, enabling compiled, low-overhead operations
  2. b) A view sharing memory with the original array
  3. b) * performs element-wise multiplication; @ performs matrix multiplication
  4. b) Summing down each column, producing 5 results
  5. False — cosine similarity, per Section 8.3, is defined by the dot product divided by the product of norms, which normalizes out magnitude, leaving only the angle (direction) between the vectors
</details>

17. Summary#

  • NumPy's ndarray stores homogeneous, fixed-type data in contiguous memory, which is the structural foundation for its dramatic performance advantage over native Python lists.
  • Indexing and slicing extend naturally to multiple dimensions, with boolean masking providing a fast, vectorized way to filter data, and a key distinction exists between views (slicing) and copies (masking, fancy indexing, .copy()).
  • Vectorized arithmetic and universal functions replace explicit loops for element-wise computation, and comparisons vectorize into boolean arrays usable directly for filtering.
  • Broadcasting extends element-wise operations to arrays of different but compatible shapes, governed by a precise trailing-dimension comparison rule.
  • The axis parameter controls which dimension an aggregation collapses, with axis=0 operating down columns and axis=1 operating across rows, a convention reused identically in Pandas.
  • Dot products, norms, and cosine similarity provide direct, interpretable analytics tools (total cost calculations, magnitude, pattern similarity) beyond their role as building blocks for machine learning.
  • Vectorization typically provides an order-of-magnitude or greater speedup over equivalent Python loops, a gap that widens further with nested loops or larger datasets, as shown in this notebook's benchmarks and case studies.

18. Further Reading#


Next Up#

Notebook 2: Pandas Fundamentals — Series, DataFrames, indexing with loc/iloc, selection, filtering, and basic file I/O, building directly on the array and vectorization concepts from this notebook, since a Pandas DataFrame is built internally on top of NumPy arrays.


Course: Data Analytics | Notebook 1 of 13

Knowledge Checkpoint

NumPy Fundamentals & Vector Math Checkpoint

Q1.Why are NumPy vectorized operations dramatically faster than standard Python loops over lists?
ANumPy ndarrays store homogeneous elements in contiguous C-level memory buffers, allowing pre-compiled C loops and SIMD CPU vector instructions to execute without CPython interpreter overhead.
BNumPy automatically executes all computations on GPUs.
CNumPy skips floating point rounding errors.
DNumPy compresses numbers using gzip in RAM.
Q2.Under NumPy broadcasting rules, when are two array dimensions compatible for element-wise arithmetic?
AWhen they are equal, or when one of the dimensions is exactly 1.
BOnly when both arrays have the exact same shape.
CWhen the sum of dimensions is even.
DWhen both arrays are 1-dimensional.
Q3.In a 2D NumPy array of shape (5, 4), what does `arr.sum(axis=0)` return?
AA 1D array of length 4 containing column sums (collapsing rows across axis 0).
BA 1D array of length 5 containing row sums.
CA single scalar representing the total sum.
DA 2D array of shape (4, 5).
Track Your Learning

Finished studying this notebook?

Mark this guide as completed to update your course progress roadmap.