Unit 4: Array Operations using NumPy

ECE181 — Introduction To Python 3 min read

NumPy (Numerical Python, first released 2006) is the foundational library for numerical computing in Python. It introduces the ndarray, an N-dimensional homogeneous container that stores values in a single contiguous block of memory and pushes element-wise work down into compiled C loops. Everything in this unit depends on the properties of that object.

  • Homogeneity: every element shares one data type (dtype), so the whole buffer has a fixed itemsize — this is what allows contiguous storage.
  • Fixed shape: an array carries a shape tuple (e.g. (3, 4)) and a ndim (number of axes); resizing means creating a new array.
  • Vectorisation: operations apply to the whole array at once with no explicit Python loop, executed in C.
  • Contiguity and strides: the array knows how many bytes to jump per axis (strides), enabling fast indexing and cheap views.
  • Import convention: the library is imported as import numpy as np throughout.

II. Arrays vs Lists

Why the ndarray replaces the built-in list for numeric work.

A. Definition and creation

An array is a homogeneous, fixed-size grid; a list is a heterogeneous, resizable sequence of pointers to objects.

  • Array construction: np.array([1, 2, 3]) builds a 1-D array; np.array([[1,2],[3,4]]) builds a 2×2 array.
  • Generators: np.zeros((2,3)), np.ones(4), np.arange(0,10,2) → [0 2 4 6 8], np.linspace(0,1,5) → 5 evenly spaced points including endpoints.
  • List by contrast: [1, "two", 3.0] legally mixes types; an array would upcast all to a common type.

B. Storage and performance

Arrays win on both memory and speed because homogeneity removes per-element overhead.

  • Memory layout: a list holds pointers to scattered Python objects; an array holds raw values in one buffer, so a million int64 values occupy ~8 MB versus far more for a list of int objects.
  • Speed: a * 2 on an array runs a single C loop; [x*2 for x in lst] interprets Python bytecode per element — typically 10–100× slower.
  • Semantics of +: for arrays a + b adds element-wise; for lists l1 + l2 concatenates.

C. Applications and limitations

Choose the container to match the data.

  • Use arrays for: large numeric datasets, matrix maths, image pixels, signal samples.
  • Use lists for: mixed types, frequent appends, small collections where flexibility beats speed.
  • Limitation: arrays are poor at growing incrementally — np.append copies the whole buffer each call.

III. Data Types

How NumPy fixes and controls the type of every element.

A. The dtype object

Each array has one dtype describing element kind and size.

  • Common dtypes: int32, int64, float32, float64, bool, complex128, and <U strings (e.g. <U5 = 5-char Unicode).
  • Inspection: a.dtype reports the type; a.itemsize gives bytes per element (float64 → 8).
  • Explicit choice: np.array([1,2,3], dtype=np.float32) forces 32-bit floats.

B. Type promotion and casting

Mixed inputs are promoted to a single type; conversions are done with astype.

  • Upcasting rule: combining int and float yields float; adding a complex yields complex.
  • Explicit cast: a.astype(np.int32) returns a new array; float→int truncates toward zero, so 2.9 → 2.
  • Overflow risk: np.array([300], dtype=np.int8) wraps around because int8 holds only −128…127.
PYTHON
np.array([1, 2, 3.5]).dtype   # dtype('float64') — int promoted to float

C. Special values

Floating-point arrays can carry non-finite markers.

  • np.nan: "not a number", used for missing data; any arithmetic with it yields nan.
  • np.inf: infinity, e.g. np.array([1.0]) / 0 → inf with a warning.
  • Detection: np.isnan(a) and np.isinf(a) return boolean masks.

IV. Array Operations

Element-wise arithmetic, indexing, reshaping and reduction on the ndarray.

A. Element-wise (vectorised) arithmetic

Arithmetic operators and ufuncs act on corresponding elements without loops.

  • Operators: a + b, a - b, a * b, a / b, a ** 2 all apply position by position.
  • Universal functions: np.sqrt(a), np.exp(a), np.sin(a) map a C-level function across every element.
  • Scalar broadcast: a + 10 adds 10 to each element (the simplest broadcasting case).
PYTHON
a = np.array([1, 2, 3])
a ** 2          # array([1, 4, 9])

B. Indexing and slicing

Arrays support list-style slicing plus multi-axis and boolean selection.

  • Basic slice: a[1:4] selects a half-open range; a[::-1] reverses.
  • 2-D indexing: m[1, 2] picks row 1, column 2; m[:, 0] takes the whole first column.
  • Boolean mask: a[a > 5] returns all elements greater than 5.
  • Views vs copies: a slice is a view sharing memory — writing to it changes the original; use a.copy() to break the link.

C. Reshaping and combining

Shape can be reinterpreted, and arrays joined, without changing the data.

  • Reshape: a.reshape(2, 3) rearranges 6 elements into a 2×3 grid; total size must match.
  • Flatten/transpose: a.ravel() returns 1-D; m.T swaps axes.
  • Stacking: np.concatenate([a, b]), np.vstack, np.hstack join along an axis.
  • -1 placeholder: a.reshape(3, -1) lets NumPy infer the second dimension.

D. Matrix operations

Linear-algebra products differ from element-wise ones.

  • Dot/matmul: A @ B or np.dot(A, B) performs matrix multiplication, contracting inner dimensions.
  • Contrast with *: A * B multiplies element-wise; A @ B sums products across a row–column pairing.
  • Helpers: np.linalg.inv(A) inverts a square matrix; np.linalg.det(A) gives the determinant.

V. Statistical Functions

Reductions that summarise an array along one axis or the whole buffer.

A. Aggregation basics

Reduction functions collapse elements to a single value or a per-axis vector.

  • Core functions: np.sum, np.mean, np.min, np.max, np.prod.
  • Method form: a.sum() equals np.sum(a).

B. The axis argument

axis controls the direction of the reduction in multi-dimensional arrays.

  • axis=0: collapse rows → one result per column (down each column).
  • axis=1: collapse columns → one result per row (across each row).
  • Omitted: reduces the whole array to a scalar.
PYTHON
m = np.array([[1, 2], [3, 4]])
m.sum(axis=0)   # array([4, 6]) — column sums
m.sum(axis=1)   # array([3, 7]) — row sums

C. Dispersion and order statistics

Beyond averages, NumPy quantifies spread and rank.

  • Spread: np.std(a) (standard deviation), np.var(a) (variance = std²).
  • Order: np.median(a), np.percentile(a, 75) for the 75th percentile.
  • Positions: np.argmin(a) and np.argmax(a) return the index of the extreme value.

D. NaN-aware and cumulative variants

Special versions ignore missing data or retain running totals.

  • NaN-safe: np.nanmean(a), np.nansum(a) skip nan instead of propagating it.
  • Cumulative: np.cumsum(a) returns running totals, np.cumprod(a) running products — output keeps the original length.

VI. Broadcasting

The rule that lets arrays of different shapes combine without explicit copying.

A. Purpose and principle

Broadcasting stretches a smaller array across a larger one so element-wise operations line up, using no extra memory.

  • Goal: avoid manually tiling data (e.g. add a row vector to every row of a matrix).
  • Mechanism: dimensions are matched conceptually; the small array is treated as repeated, but no physical copy is made.

B. The broadcasting rules

Two shapes are compatible when their dimensions align under fixed rules, compared from the right.

  • Rule 1 — align right: shapes are compared trailing-dimension first; missing leading dimensions are treated as 1.
  • Rule 2 — stretch 1s: a dimension of size 1 is stretched to match the other; otherwise sizes must be equal.
  • Rule 3 — else error: incompatible sizes raise ValueError: operands could not be broadcast together.
PYTHON
# (3,3) matrix + (3,) row vector
m = np.ones((3, 3))
r = np.array([1, 2, 3])
m + r          # r stretched down all 3 rows

C. Worked example — column vs row broadcast

Reshaping one operand changes how it stretches.

  • Row vector (1,3): added to a (3,3) matrix, it repeats down each row.
  • Column vector (3,1): added to the same matrix, it repeats across each column.
PYTHON
col = np.array([[10], [20], [30]])   # shape (3,1)
m + col        # 10 added to row 0, 20 to row 1, 30 to row 2

D. Applications and limitations

Broadcasting underlies most vectorised numeric code but has boundaries.

  • Common uses: mean-centering data (data - data.mean(axis=0)), scaling features, outer products via a[:, None] * b[None, :].
  • Efficiency: no intermediate expanded array is built, so memory stays low.
  • Limitation: shapes like (3,) and (4,) cannot broadcast — mismatched non-1 dimensions always fail, and silent shape mistakes can produce unintended large arrays.