Unit 7: Operations on NumPy arrays

ECAP776 9 min read

I. Orientation — The ndarray Model

NumPy (Numerical Python, first released as NumPy in 2006) organizes numerical data in homogeneous, multidimensional arrays called ndarray objects. Operations generally act on complete arrays rather than requiring explicit Python loops, making numerical programs concise and computationally efficient.

  • Homogeneous storage: Every element normally has the same NumPy data type, or dtype, such as int64, float32, or bool.
  • Dimensions and axes: An array’s ndim is its number of dimensions; axes are numbered from 0.
    • In shape (r, c), axis 0 has r rows and axis 1 has c columns.
    • For shape (d, r, c), axis 0 indexes d two-dimensional layers.
  • Shape: The tuple a.shape records the length of each axis. An array of shape (2, 3) has two rows and three columns.
  • Size: a.size gives the total number of elements:
    TEXT
      size = s₀ × s₁ × ... × sₙ₋₁

    Here, sᵢ is the length of axis i, and n is the number of dimensions.
  • Vectorization: Arithmetic and many functions operate element by element in compiled NumPy code, avoiding slower explicit Python iteration.
  • Views and copies: Some manipulations return a view sharing memory with the original array, while others allocate an independent copy.
  • Element-wise default: Operators such as +, -, *, /, and comparisons usually pair corresponding elements; matrix multiplication instead uses @.
  • Broadcast compatibility: Arrays with unequal shapes may still interact when their trailing dimensions are equal or one of them is 1.
  • Conventional import: NumPy is ordinarily imported using:
    PYTHON
      import numpy as np

II. Array Manipulation — Changing Shape, Order, and Structure

A. Array manipulation

Array manipulation changes how array data is shaped, indexed, arranged, joined, or divided, usually without changing the underlying numerical meaning of its elements.

  • Reshaping: reshape() changes dimensions while preserving the total number of elements.

    PYTHON
      a = np.arange(12)
      b = a.reshape(3, 4)
    • a.shape is (12,), while b.shape is (3, 4).
    • A reshape from shape S to shape T is valid only when product(S) = product(T).
    • One dimension may be -1, allowing NumPy to infer it: a.reshape(3, -1) produces shape (3, 4).
  • Flattening: Multidimensional data can be converted into one dimension.

    • a.flatten() returns an independent copy.
    • a.ravel() returns a flattened view when memory layout permits, though it may return a copy.
      PYTHON
        m = np.array([[1, 2], [3, 4]])
        x = m.flatten()   # array([1, 2, 3, 4])
  • Transposition: a.T reverses the order of axes; np.transpose(a, axes) permits an explicit axis order.

    PYTHON
      a = np.array([[1, 2, 3], [4, 5, 6]])
      b = a.T


    Here, a.shape is (2, 3) and b.shape is (3, 2). For a one-dimensional array, .T does not create a column vector; a.reshape(-1, 1) is required.

  • Axis movement: Functions rearrange dimensions without altering element values.

    • np.swapaxes(a, i, j) exchanges axes i and j.
    • np.moveaxis(a, source, destination) moves a selected axis.
    • For image batches shaped (batch, height, width, channels), moving channels to axis 1 gives (batch, channels, height, width).
  • Adding and removing dimensions: Dimensions of length 1 help prepare data for broadcasting.

    PYTHON
      x = np.array([10, 20, 30])
      row = x[np.newaxis, :]   # shape (1, 3)
      col = x[:, np.newaxis]   # shape (3, 1)
    • np.expand_dims(x, axis=k) inserts a new axis at position k.
    • np.squeeze(a, axis=k) removes an axis of length 1; attempting to squeeze a longer axis raises an error.
  • Joining arrays: Concatenation combines arrays along an existing axis.

    PYTHON
      a = np.array([[1, 2]])
      b = np.array([[3, 4]])
      np.concatenate((a, b), axis=0)
      # array([[1, 2],
      #        [3, 4]])


    Arrays must have matching dimensions except along the joining axis. np.stack() differs by creating a new axis; vstack(), hstack(), and column_stack() are convenient specialized forms.

  • Splitting arrays: np.split(a, sections, axis) separates an array into equal parts, while np.array_split() permits unequal part sizes.

    • np.hsplit() divides by columns for a two-dimensional array.
    • np.vsplit() divides by rows.
    • Exact split() raises an error if the selected axis cannot be divided evenly.
  • Repetition and rearrangement: np.repeat() repeats individual elements, whereas np.tile() repeats an entire pattern.

    PYTHON
      np.repeat([1, 2], 2)       # array([1, 1, 2, 2])
      np.tile([1, 2], 2)         # array([1, 2, 1, 2])

B. Applications and limitations

Manipulation operations prepare arrays for computation, but correct shape and memory behavior must be checked explicitly.

  • Data preparation: Reshaping converts a flat sequence of 12 sensor values into, for example, 3 observations with 4 features each.
  • Memory sharing: Modifying a view may modify its source.
    PYTHON
      a = np.arange(4)
      b = a.reshape(2, 2)
      b[0, 0] = 99

    Because b commonly shares storage with a, a[0] becomes 99; b = a.reshape(2, 2).copy() prevents this coupling.
  • Shape safety: Arrays can contain the same number of elements yet represent different structures; shape (2, 6) is not semantically interchangeable with (3, 4).
  • Performance: Views are generally cheap because they reuse memory, while copying, concatenating, and repeatedly appending arrays allocate storage.
  • Append limitation: np.append() returns a new array rather than extending an array in place; collecting values in a list and converting once is often more efficient.

III. Broadcasting — Operating Across Compatible Shapes

A. Broadcasting

Broadcasting is NumPy’s rule for performing element-wise operations on arrays of different but compatible shapes without explicitly copying repeated values.

  • Comparison rule: Shapes are compared from their rightmost dimensions toward the left. Two dimensions are compatible when:

    1. their lengths are equal; or
    2. one length is 1.
  • Missing dimensions: If one shape has fewer axes, leading dimensions of length 1 are conceptually inserted. Thus (3,) is treated like (1, 3) when paired with (2, 3).

  • Result shape: Each result dimension takes the larger compatible length. Combining (4, 1, 3) with (1, 5, 3) produces (4, 5, 3).

  • Scalar broadcasting: A scalar behaves as though it had the same shape as the array.

    PYTHON
      a = np.array([2, 4, 6])
      a * 10                 # array([20, 40, 60])


    The scalar 10 is conceptually paired with every element; NumPy does not need to construct [10, 10, 10].

  • Worked example: A column vector and row vector broadcast to form a grid.

    PYTHON
      col = np.array([[1], [2], [3]])   # shape (3, 1)
      row = np.array([10, 20, 30, 40])  # shape (4,)
      result = col + row                 # shape (3, 4)
    TEXT
      [[11, 21, 31, 41],
       [12, 22, 32, 42],
       [13, 23, 33, 43]]


    The column expands across four columns, while the row is treated as shape (1, 4) and expands across three rows.

  • Incompatibility: Shapes (2, 3) and (2,) fail because their rightmost dimensions are 3 and 2; neither is 1. Reshaping the second array to (2, 1) makes row-wise broadcasting possible.

  • No physical tiling: Broadcasting conceptually stretches dimensions of length 1, ordinarily avoiding the memory cost of np.tile().

B. Applications and limitations

Broadcasting supports concise numerical formulas, but accidental compatibility can also produce unintended results.

  • Feature scaling: For data X of shape (m, n), subtracting a mean vector μ of shape (n,) centers every feature:

    PYTHON
      centered = X - mu


    Here, m is the number of observations, n is the number of features, and μ[j] is the mean of feature j.

  • Pairwise computation: Reshaping one vector as (m, 1) and another as (1, n) creates all m × n pairwise combinations.

  • Efficiency: Broadcasting avoids explicit Python loops and unnecessary repeated arrays, typically improving speed and memory usage.

  • Memory risk: Although operands are not tiled, the result is materialized. Broadcasting shapes (100000, 1) and (1, 100000) would request a result containing 10¹⁰ elements.

  • Intent clarity: np.newaxis or reshape() should make the intended axis explicit. A vector of shape (3,) may represent rows, columns, or features depending on context.

  • Assignment restriction: An in-place operation cannot change the left operand’s shape. An array of shape (3, 1) cannot store an in-place result of shape (3, 4).

IV. Binary Operators — Combining Two Operands

A. Binary operators

Binary operators combine two operands—arrays, array-like objects, or scalars—and generally produce an element-wise result governed by broadcasting.

  • Arithmetic operators: NumPy supports +, -, *, /, //, %, and **.

    PYTHON
      a = np.array([5, 8, 11])
      b = np.array([2, 3, 4])
    • a + b gives [7, 11, 15].
    • a * b gives [10, 24, 44].
    • a / b performs true division.
    • a // b performs floor division, rounding toward negative infinity.
    • a % b gives remainders, and a ** b performs element-wise exponentiation.
  • Operator-function equivalents: Operators correspond to NumPy universal functions, or ufuncs.

    • a + b corresponds to np.add(a, b).
    • a * b corresponds to np.multiply(a, b).
    • a ** b corresponds to np.power(a, b).
      These functions may support parameters such as where= and out=.
  • Comparison operators: ==, !=, <, <=, >, and >= return Boolean arrays.

    PYTHON
      a >= 8   # array([False, True, True])


    Whole-array conditions use np.all(condition) or np.any(condition); directly using a multi-element Boolean array in if is ambiguous and raises an error.

  • Logical operators: np.logical_and, np.logical_or, np.logical_not, and np.logical_xor operate on truth values.

    PYTHON
      mask = (a >= 8) & (a < 11)


    Parentheses are necessary because & has different precedence from comparisons. Python’s and and or do not perform element-wise array logic.

  • Bitwise operators: Integer and Boolean arrays support &, |, ^, ~, <<, and >>.

    • &, |, and ^ combine corresponding bits.
    • << k and >> k shift each integer by k bit positions.
    • For Boolean arrays, &, |, and ^ act as element-wise logical combinations.
  • Matrix multiplication versus multiplication:

    1. Element-wise multiplication: A * B multiplies corresponding broadcast-compatible elements.
    2. Matrix multiplication: A @ B, equivalent to np.matmul(A, B), contracts matching inner dimensions.

    If A has shape (m, n) and B has shape (n, p), then A @ B has shape (m, p):

    TEXT
      Cᵢⱼ = Σₖ AᵢₖBₖⱼ


    Here, i indexes rows of A, j indexes columns of B, k spans the shared dimension n, and C is the product.

B. Applications and limitations

Binary operations provide vectorized numerical and conditional processing, while data types and exceptional values influence their results.

  • Boolean selection: A comparison creates a mask for filtering:
    PYTHON
      values = np.array([3, -1, 7, -4])
      positive = values[values > 0]   # array([3, 7])
  • Type promotion: Combining different dtypes produces a common result dtype; adding an integer array to a floating-point array ordinarily yields floating-point values.
  • In-place casting: a += b attempts to store the result in a’s existing dtype. Adding floating-point values to an integer array in place may raise a casting error rather than silently discard decimals.
  • Exceptional arithmetic: Division by zero and invalid floating-point operations can produce inf, -inf, or nan with warnings. np.isfinite(), np.isinf(), and np.isnan() detect these values.
  • Floating-point equality: Rounding error makes a == b unsuitable for many computed decimal values; np.isclose(a, b) and np.allclose(a, b) compare within tolerances.
  • Precedence: Parenthesized expressions such as (a > 0) & (a < 10) prevent operators from being grouped incorrectly and clearly communicate the intended element-wise condition.