1. Introduction: Accessing and Manipulating Array Subsets
In Python data analysis, working with NumPy requires not just creating arrays, but also extracting, slicing, and modifying specific portions of them. NumPy provides specialized techniques—including zero-based indexing, negative indexing, slicing, and strides—to access individual elements or entire blocks of a multidimensional matrix without unnecessary loops.
Example 3×3 matrix layout used throughout this lesson:
Col 0 Col 1 Col 2
Row 0 → 10 20 30
Row 1 → 40 50 60
Row 2 → 70 80 90
2. Setup and Basic Indexing in a 2D Array
NumPy is conventionally imported as np. A two-dimensional array with 3 rows and 3 columns can be defined using np.array():
import numpy as np
# Creating a 3x3 two-dimensional NumPy array
arr = np.array([
[10, 20, 30], # Row 0
[40, 50, 60], # Row 1
[70, 80, 90] # Row 2
])
Zero-Based Indexing
Like standard Python data structures, NumPy indexing starts at index 0. To access a specific element in a 2D array, specify its coordinate as [row_index, column_index]:
# Accessing specific elements
print("Element at Row 0, Column 1:", arr[0, 1]) # Output: 20
print("Element at Row 2, Column 2:", arr[2, 2]) # Output: 90
arr[0, 1]: row 0, column 1 → 20
arr[2, 2]: last row and last column → 90
3. Negative Indexing: Navigating from the End
NumPy supports negative indexing, allowing elements to be referenced relative to the end of the matrix:
# Negative indexing examples
print("Element at [-1, -2]:", arr[-1, -2]) # Output: 80
print("Element at [-3, -3]:", arr[-3, -3]) # Output: 10
arr[-1, -1]: bottom-right element (last row, last column)
arr[-1, -2]: last row, second-to-last column → 80
arr[-3, -3]: maps to top-left at row 0, column 0 → 10
4. Slicing Subsets of an Array (start:stop)
Slicing extracts a rectangular sub-matrix using the syntax [row_start:row_stop, col_start:col_stop].
Inclusion and Exclusion Rules
In slicing ranges, the start index is included, but the stop index is excluded:
0:2 selects rows 0 and 1 (index 2 excluded)
1:3 selects columns 1 and 2 (index 3 excluded)
arr[0:2, 1:3] extracts a 2×2 sub-array: 20, 30, 50, 60
# Slicing rows 0 to 1 and columns 1 to 2
sub_array = arr[0:2, 1:3]
print("Sliced 2x2 Sub-array:\n", sub_array)
# Output:
# [[20 30]
# [50 60]]
5. Strides: Skipping Elements with Step Sizes (::step)
Strides allow skipping rows and columns using a step value. Using ::2 instructs NumPy to jump two steps forward each time.
# Extracting every second row and every second column
strided_arr = arr[::2, ::2]
print("Strided Sub-array (Corners):\n", strided_arr)
# Output:
# [[10 30]
# [70 90]]
arr[::2, ::2] selects every second row and every second column, yielding the four outer corner elements: 10, 30, 70, and 90.
6. Critical Concept: Views vs. Independent Copies
Standard Slices Are Views (References)
When creating a slice in NumPy, it does not create a new independent array in memory. Instead, it creates a view (reference) pointing to the original array. Modifying an element in a sliced view directly modifies the original array:
# Creating a slice view
slice_arr = arr[0:2, 0:2]
# Modifying the first element of the slice
slice_arr[0, 0] = 999
# Inspecting the original array
print("Original Array after modifying slice:")
print(arr)
# Element at row 0, column 0 of the original array is now 999
Preventing Unintended Modifications with .copy()
If modifications to a slice must not alter the original data, create an independent copy with the .copy() method:
# Resetting and creating an explicit independent copy
arr[0, 0] = 10
slice_copy = arr[0:2, 0:2].copy()
# Modifying an element inside the independent copy
slice_copy[0, 0] = 555
print("Slice Copy:\n", slice_copy)
print("Original Array (unaltered):\n", arr)
# 555 appears only in slice_copy; original remains 10