{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "---\n", "title: \"Chapter 7: NumPy advanced\"\n", "---" ] }, { "cell_type": "markdown", "id": "1", "metadata": {}, "source": [ "[](https://colab.research.google.com/github/sambaiga/ds-mlops-path/blob/main/tutorials/01-python-basics/07-numpy-advanced.ipynb) [](https://raw.githubusercontent.com/sambaiga/ds-mlops-path/main/tutorials/01-python-basics/07-numpy-advanced.ipynb)" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "Subtracting a shape-`(3,)` mean from a shape-`(5, 3)` matrix should raise an error. The shapes do not match. NumPy does it anyway, without complaining, without copying data. If you do not know why, you will write code that works by accident until the shapes change and it suddenly fails in production with no obvious cause.\n", "\n", "The answer is broadcasting: NumPy's rule for combining arrays of different shapes by stretching size-1 dimensions without copying data. Once you understand that rule, the gap between code that works and code that works *quickly* closes fast.\n", "\n", "The key concept is broadcasting. Once you understand it, the rest follows: memory layout, vectorisation, linear algebra, saving and loading arrays, and the gotchas that bite you when shapes are almost right but not quite. Chapter 6 built the array fundamentals; this chapter shows what those arrays can do at speed. By the end you will have everything you need for Chapter 14: matplotlib visualisations of the arrays you have been building.\n", "\n", "> Callout markers: [book cover page](../../index.qmd#callout-guide)." ] }, { "cell_type": "markdown", "id": "3", "metadata": {}, "source": [ "::: {.callout-note collapse=\"true\" icon=false}\n", "## Learning Objectives\n", "\n", "| # | Skill | Covered in |\n", "|---|---|---|\n", "| 1 | Apply broadcasting rules to combine arrays of different shapes | Sec. 7 |\n", "| 2 | Interpret `arr.strides` and `arr.itemsize` to understand memory layout | Sec. 7 |\n", "| 3 | Replace Python loops with vectorised NumPy expressions | Sec. 8 |\n", "| 4 | Compute dot products and matrix operations with `@` | Sec. 9 |\n", "| 5 | Save and load arrays with `.npy` and `.npz` | Sec. 10 |\n", "| 6 | Identify and fix the three most common NumPy gotchas | Sec. 11 |\n", ":::" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, "source": [ "## Setup\n", "\n", "Re-run this cell to restore the imports and student data matrix from Chapter 6." ] }, { "cell_type": "code", "execution_count": null, "id": "5", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "# Student data matrix: 5 students x 3 measurements (study_hours, attendance_pct, prior_gpa)\n", "student_data = np.array(\n", " [\n", " [12.0, 85.0, 3.1],\n", " [5.0, 60.0, 2.4],\n", " [18.0, 95.0, 3.8],\n", " [9.0, 70.0, 2.9],\n", " [22.0, 98.0, 3.9],\n", " ]\n", ")\n", "column_names = [\"study_hours\", \"attendance_pct\", \"prior_gpa\"]\n", "print(f\"student_data.shape={student_data.shape} student_data.dtype={student_data.dtype}\")" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "## 7. Broadcasting\n", "\n", "**Broadcasting** is the rule NumPy uses to apply an operation between two arrays of *different* shapes, by virtually \"stretching\" the smaller one, without actually copying any data. It is what lets you write `student_data - student_data.mean(axis=0)` instead of a loop over rows.\n", "\n", "
(5, 3) and (3,) → treat (3,) as (1, 3) → stretch to (5, 3). (valid)(5, 3) and (5,) → treat (5,) as (1, 5) → 3 != 5 and neither is 1. (error)\n",
"per_row_mean = student_data.mean(axis=1) (shape (5,)) and try student_data - per_row_mean, NumPy raises ValueError: operands could not be broadcast together. It is comparing the trailing dimensions 3 vs 5, not what you intended. Fix it by giving the per-row result an explicit column shape with keepdims=True: student_data.mean(axis=1, keepdims=True) has shape (5, 1), which broadcasts correctly against (5, 3).\n",
"float64 array of shape (5, 3): itemsize is 8 bytes, strides are (24, 8). Moving one column right costs 8 bytes (= itemsize). Moving one row down costs 24 bytes (= 3 columns × 8 bytes). Basic slicing adjusts strides without copying data: that is why reshape() and [::-1] return views instantly.\n",
"student_data to the [0, 1] range using the formula (student_data - student_data.min(axis=0)) / (student_data.max(axis=0) - student_data.min(axis=0)). Confirm the result's per-column min is 0 and max is 1.\n",
"student_data_scaled.min(axis=0) # -> array([0., 0., 0.])\n",
"student_data_scaled.max(axis=0) # -> array([1., 1., 1.])\n",
"(arr - arr.mean()) / arr.std() takes ~5ms. NumPy calls compiled C code: the Python interpreter never enters the inner loop. The speedup grows with array size.\n",
"for loop over an array, stop and look for a vectorised waynp.where, boolean masks, axis aggregations. Reach for those first: a hand-written loop over a large array is one of the most common NumPy performance bugs, and it's usually a 10-100x slowdown for no benefit.\n",
"arr = np.random.default_rng(0).normal(size=100_000).(arr - arr.mean()) / arr.std().import time and print the speedup ratio.student_data @ w computes every row's dot product with w in one call. Manually looping for row in student_data: sum(row * w) is correct but 100x slower. Every linear model, transformer attention layer, and PCA uses @ internally.\n",
"np.allclose(a, b) was used above instead of (a == b).all() on purpose: floating-point arithmetic accumulates tiny rounding errors, so two mathematically-equal results can differ in their last bit. Always compare floats with a tolerance: np.allclose, or abs(a - b) < 1e-9, never with exact ==.\n",
"student_data from the setup section:weights = np.array([0.3, 0.4, 1.5]) and bias = -2.0.preds = student_data @ weights + bias.preds.shape == (5,).np.argmax(preds).\n",
"np.save('student_data.npy', student_data) and np.load('student_data.npy') preserve dtype and shape exactly. np.savez('data.npz', features=student_data, target=y) bundles multiple arrays in one file. Both load in microseconds: much faster than re-parsing a CSV.\n",
"sub = student_data[:2] shares memory; mutating sub mutates student_data. Fix: student_data[:2].copy().np.array([120, 10], dtype=np.int8) wraps silently. Fix: check dtype before arithmetic.(5, 3) - (3, 5) may broadcast unexpectedly. Always check shapes before subtracting.\n",
"2 standard deviations from the overall mean using a single boolean mask.\n",
"readings = [36.5, 36.7, 36.8, 36.6, 36.9, 39.5, 36.7, 36.8]\n",
"# Expected: reading 39.5 (index 5) flagged as anomaly\n",
"Hint: z = (readings - readings.mean()) / readings.std(), then mask np.abs(z) > 2.\n",
"np.float64 not np.float in new codemodule 'numpy' has no attribute 'float' error, the codebase was written against NumPy 1.x. A global search-and-replace of np.float → np.float64 (and so on for the others in the table) is the complete fix.\n",
"np.dtypes.StringDType() for string arrays that need fast operationsnp.array([\"a\", \"b\"]) stores strings as object dtype: each element is a Python object pointer, so any operation loops at the Python level. np.dtypes.StringDType() stores strings as contiguous UTF-8 bytes, avoids the object-dtype overhead, and unlocks the entire np.strings.* API for compiled, vectorised string work.\n",
""
]
},
{
"cell_type": "markdown",
"id": "48",
"metadata": {},
"source": [
"## Further Reading\n",
"\n",
"| Resource | Why it matters |\n",
"|---|---|\n",
"| Harris, C.R. et al. (2020). [Array programming with NumPy](https://doi.org/10.1038/s41586-020-2649-2). *Nature* 585, 357–362. | The primary citation for NumPy; the paper explains the design decisions behind broadcasting and ufuncs |\n",
"| VanderPlas, J. (2016). *Python Data Science Handbook*, Ch. 2. O'Reilly. | Free online: the most readable treatment of fancy indexing, broadcasting, and structured arrays |\n",
"| [NumPy documentation: Broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html) | Official broadcasting rules with diagrams; bookmark for the next time the shapes don't align |\n",
"| [NumPy documentation: Indexing](https://numpy.org/doc/stable/user/basics.indexing.html) | Covers basic, advanced, and boolean indexing in one place |\n"
]
},
{
"cell_type": "markdown",
"id": "49",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"| Concept | Key rule |\n",
"|---|---|\n",
"| `ndarray` | One dtype, contiguous memory, fast because it skips the Python interpreter |\n",
"| Creation | `np.array`, `arange`, `linspace`, `zeros`/`ones`; `np.random.default_rng(seed)` for reproducible random data |\n",
"| `shape`/`dtype` | Always check before trusting a result; mixed-type input silently upcasts |\n",
"| `reshape` | Returns a **view**, same data, same `size`, different shape |\n",
"| `flatten` | Always returns a **copy** |\n",
"| Slicing | Basic slices (`student_data[:, 0]`) are views; fancy/boolean indexing always copies |\n",
"| Boolean masks | `&` / `\\|` (not `and`/`or`) on arrays, each side parenthesised |\n",
"| `np.where` / `np.select` | Vectorised if/else and multi-branch labelling |\n",
"| `axis=0` vs `axis=1` | 0 collapses rows -> one value per **column**; 1 collapses columns -> one value per **row** |\n",
"| Broadcasting | Compare shapes right-to-left; dims match if equal or one is `1`; use `keepdims=True` to broadcast a per-row stat |\n",
"| Vectorisation | A NumPy expression beats a Python loop by 10-100x, look for one before writing `for` |\n",
"| `@` / `np.dot` | Matrix multiplication: `student_data @ weights` predicts every row in one call |\n",
"| `np.allclose` | Always compare floats with a tolerance, never `==` |\n",
"| `.npy` / `.npz` | Compact, dtype/shape-preserving array storage between pipeline stages |\n",
"\n",
"\n",
"**Next:** Chapter 14 (`14-matplotlib.ipynb`), covering how to visualise arrays and DataFrames with matplotlib."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "ark",
"language": "python",
"name": "ark"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}