Unit 6: Introduction to NumPy

ECAP776 8 min read

I. Foundations of NumPy

NumPy, short for Numerical Python, is a Python library for efficient numerical computation. Its central object is the multidimensional ndarray, whose elements normally have one fixed data type and occupy a structured block of memory.

  • Governing principle: NumPy performs operations on whole arrays rather than requiring explicit Python loops for every element; this approach is called vectorization.
  • Import convention: NumPy is conventionally imported with the alias np.
PYTHON
import numpy as np
  • Array dimensions: An array may have zero, one, two, or more axes.
    • A scalar array has zero axes.
    • A vector-like array has one axis.
    • A matrix-like array has two axes.
    • Higher-dimensional arrays have three or more axes.
  • Homogeneous storage: Elements usually share one dtype, such as int64, float64, or bool.
  • Shape convention: shape is a tuple giving the length of each axis; an array with three rows and four columns has shape (3, 4).
  • Size convention: size gives the total number of elements. For shape (3, 4), the size is (3 \times 4 = 12).
  • Axis convention: In a two-dimensional array, axis 0 runs down the rows and axis 1 runs across the columns.
  • Core attributes:
PYTHON
a = np.array([[1, 2, 3], [4, 5, 6]])

a.ndim       # 2: number of axes
a.shape      # (2, 3): axis lengths
a.size       # 6: total number of elements
a.dtype      # integer type, platform-dependent
a.itemsize   # bytes occupied by one element

II. NumPy Arrays and Python Lists — Storage and Computation

A. Arrays vs lists

Arrays and lists can both represent collections, but they differ substantially in storage rules, supported operations, and intended use.

  1. Python lists
    • General-purpose container: A list can contain references to values of unrelated types, such as an integer, string, and Boolean.
PYTHON
items = [10, "NumPy", True]
  • Flexible structure: Lists can grow or shrink through methods such as append(), extend(), and pop().
  • Sequence behavior: Multiplication repeats a list rather than multiplying its elements.
PYTHON
values = [1, 2, 3]
values * 2             # [1, 2, 3, 1, 2, 3]
  • Elementwise work: Arithmetic across all elements generally requires a loop or comprehension.
PYTHON
doubled = [x * 2 for x in values]
  1. NumPy arrays
    • Numerical container: An ndarray stores values in a regular, multidimensional structure and normally uses one data type.
    • Fixed size: An array’s total storage is not dynamically extended like a list; operations that appear to add elements usually create a new array.
    • Elementwise behavior: Arithmetic operators act on corresponding elements.
PYTHON
a = np.array([1, 2, 3])
a * 2                  # array([2, 4, 6])
a + 10                 # array([11, 12, 13])
  • Vectorization: NumPy executes many array operations in optimized compiled code, avoiding the overhead of a Python-level loop.
  • Compact storage: Values of one fixed-width dtype can be stored more compactly than separate Python objects referenced by a list.
  • Multidimensional model: A single array supports dimensions directly, whereas nested lists only imitate rows and columns.
PYTHON
matrix = np.array([[1, 2], [3, 4]])
matrix.shape            # (2, 2)
  • Type conversion: When given mixed numeric types, NumPy generally selects a common type capable of representing them.
PYTHON
a = np.array([1, 2.5, 3])
a.dtype                 # commonly float64
  • Broadcasting: Compatible arrays of different shapes may interact without manually copying values; adding scalar 5 to shape (3,) applies 5 to all three elements.
  • Trade-off: Lists are preferable for heterogeneous, frequently changing collections; arrays are preferable for regular numerical data and bulk computation.
  • Important distinction: An array is not a subclass of list, even when both display similar sequences of values.

III. Constructing Structured Arrays — NumPy-Supplied Patterns

A. Array creation routines

Array creation routines generate arrays with specified shapes, values, ranges, or patterns without requiring all elements to be entered individually.

  • Zero-filled arrays: np.zeros(shape, dtype) initializes every element to zero.
PYTHON
z = np.zeros((2, 3), dtype=int)
# array([[0, 0, 0],
#        [0, 0, 0]])
  • shape=(2, 3) means two rows and three columns.
  • dtype=int requests integer elements.
  • One-filled arrays: np.ones(shape, dtype) initializes every element to one.
PYTHON
o = np.ones(4, dtype=float)
# array([1., 1., 1., 1.])
  • Constant-filled arrays: np.full(shape, fill_value) repeats a specified value.
PYTHON
f = np.full((2, 2), 7)
# array([[7, 7],
#        [7, 7]])
  • Uninitialized arrays: np.empty(shape, dtype) allocates storage without setting predictable values.

    • Its contents depend on the existing memory state.
    • It is useful only when every element will be overwritten before being read.
  • Integer-like ranges: np.arange(start, stop, step) creates evenly stepped values and excludes stop.

PYTHON
r = np.arange(2, 11, 2)
# array([ 2, 4, 6, 8, 10])
  • start is the first value, here 2.
  • stop is the excluded boundary, here 11.
  • step is the increment, here 2.
  • Evenly spaced intervals: np.linspace(start, stop, num) creates exactly num values and includes both endpoints by default.
PYTHON
x = np.linspace(0, 1, 5)
# array([0.  , 0.25, 0.5 , 0.75, 1.  ])
  • num=5 fixes the number of samples.
  • The spacing is ((1-0)/(5-1)=0.25).
  • Identity matrix: np.eye(n) creates a square two-dimensional array with ones on the main diagonal and zeros elsewhere.
PYTHON
identity = np.eye(3)
# array([[1., 0., 0.],
#        [0., 1., 0.],
#        [0., 0., 1.]])
  • Shape-matching routines: np.zeros_like(a), np.ones_like(a), and np.full_like(a, value) derive shape and usually dtype from an existing array a.
  • Data-type control: Supplying dtype=np.float32, for example, fixes each element’s representation rather than relying on inference.
  • Routine choice: Use arange when the step is central and linspace when the number of samples is central; linspace is generally safer for fractional intervals because floating-point steps may make an arange endpoint unintuitive.

IV. Converting Available Collections — Input and Copying

A. Arrays from existing data

NumPy can construct arrays from Python sequences and other array-like objects while inferring shape and data type from their contents.

  • Basic conversion: np.array(object) converts a sequence into a new ndarray.
PYTHON
temperatures = [18.5, 20.0, 21.5]
a = np.array(temperatures)

a.shape                 # (3,)
a.dtype                 # commonly float64
  • Nested sequences: Equally sized inner sequences become dimensions of a rectangular array.
PYTHON
data = [[1, 2, 3], [4, 5, 6]]
matrix = np.array(data)

matrix.shape             # (2, 3)
matrix.ndim              # 2
  • Rectangular requirement: Rows intended for a standard numeric array must have consistent lengths. Ragged nested sequences do not form an ordinary two-dimensional numeric array.
  • Explicit type: The dtype argument converts values during construction when conversion is valid.
PYTHON
a = np.array([1, 2, 3], dtype=float)
# array([1., 2., 3.])
  • Array conversion: np.asarray(data) converts array-like input but may return the original array when no conversion is needed.
    • np.array(data) creates a copy by default in ordinary use.
    • np.asarray(data) is useful when avoiding an unnecessary copy is desirable.
  • Copying explicitly: a.copy() creates an independent array whose later modifications do not affect a.
PYTHON
a = np.array([10, 20, 30])
b = a.copy()
b[0] = 99

a                       # array([10, 20, 30])
  • Text and buffer sources: Routines such as np.fromstring() and np.frombuffer() can interpret structured existing data, but the caller must supply suitable separators, types, or buffer formats.
  • Worked conversion: A flat sequence can be reshaped when its element count matches the requested dimensions.
PYTHON
a = np.array([1, 2, 3, 4, 5, 6])
matrix = a.reshape(2, 3)
# array([[1, 2, 3],
#        [4, 5, 6]])
  • The original size is 6.
  • The requested size is (2 \times 3=6).
  • A shape such as (4, 2) would fail because it requires eight elements.

V. Selecting Array Data — Positions, Ranges, and Conditions

A. Indexing and slicing

Indexing selects individual elements or groups of elements, while slicing selects regularly spaced ranges along one or more axes.

  • Zero-based indexing: The first position is index 0; negative indices count backward from the end.
PYTHON
a = np.array([10, 20, 30, 40])
a[0]                    # 10
a[-1]                   # 40
  • Slice notation: a[start:stop:step] includes start, excludes stop, and advances by step.
PYTHON
a[1:4:2]                # array([20, 40])
a[::-1]                 # array([40, 30, 20, 10])
  • Omitted boundaries: In a[:3], the start defaults to the beginning; in a[2:], the stop defaults to the end.
  • Multidimensional indexing: Comma-separated indices specify one position per axis.
PYTHON
m = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])

m[1, 2]                 # 6
m[0, :]                 # array([1, 2, 3])
m[:, 1]                 # array([2, 5, 8])
m[0:2, 1:3]             # array([[2, 3],
                         #        [5, 6]])
  • Dimensional effect: m[0, :] removes the indexed row axis and returns shape (3,), whereas m[0:1, :] preserves it and returns shape (1, 3).
  • Slice views: Basic slices commonly share memory with the source array, so assigning through the slice can modify the original.
PYTHON
a = np.array([1, 2, 3, 4])
part = a[1:3]
part[0] = 99

a                       # array([1, 99, 3, 4])
  • Safe independence: Use a[1:3].copy() when changes to the selected data must not affect the source.
  • Boolean indexing: A Boolean mask selects elements for which the corresponding condition is True.
PYTHON
a = np.array([2, 7, 4, 9])
mask = a > 5
a[mask]                 # array([7, 9])
  • Fancy indexing: An integer array or list selects specified positions, generally producing a copy rather than a basic slice view.
PYTHON
a[[3, 0, 2]]            # array([9, 2, 4])
  • Assignment through selection: A scalar can be assigned to every selected location.
PYTHON
a[a > 5] = 0
# array([2, 0, 4, 0])
  • Boundary rule: Slice endpoints may extend beyond the array without raising an error, but an individual out-of-range index raises IndexError.