NumPy Indexing on ndarrays

NumPyBeginner
Practice Now

Introduction

Welcome back! Now that you understand how to create NumPy arrays, it's time to learn how to access and manipulate the data within them. In this lab, you'll explore indexing - the primary method for accessing and modifying data within NumPy arrays.

Building on What You Learned

In the previous lab, you learned:

  • How to create arrays using various NumPy functions
  • The difference between arrays and Python lists
  • Array dimensions (1D, 2D, 3D) and data types

Now you'll learn how to:

  • Access individual elements or groups of elements
  • Extract specific rows, columns, or regions from arrays
  • Use advanced selection techniques for complex data filtering
  • Modify array data through indexing

Why Indexing Matters

Indexing is fundamental to data manipulation because it allows you to:

  • Extract subsets of data for analysis
  • Modify specific values in large datasets
  • Filter data based on conditions
  • Perform vectorized operations on selected elements

These skills are essential for any data analysis or scientific computing task in Python.

Basic Indexing and Slicing in 1D Arrays

Now that you know how to create arrays, let's learn how to access their contents. All your work in this lab will be done in the indexing_practice.py file.

Understanding Array Indexing

Zero-Based Indexing

Like Python lists, NumPy arrays use zero-based indexing. This means:

  • The first element is at index 0
  • The second element is at index 1
  • And so on...

Visualizing Array Indices

For an array x = [10, 20, 30, 40, 50]:

Index:  0   1   2   3   4
Value: 10  20  30  40  50

So x[0] gives 10, x[2] gives 30, etc.

Slicing Syntax

Slicing lets you select a range of elements with the syntax start:stop:step:

  • start: Index where the slice begins (inclusive)
  • stop: Index where the slice ends (exclusive)
  • step: How many elements to skip (optional, defaults to 1)

Common slicing patterns:

  • x[1:4]: Elements at indices 1, 2, 3
  • x[:3]: First 3 elements (indices 0, 1, 2)
  • x[2:]: From index 2 to the end
  • x[::2]: Every other element starting from index 0
  • x[::-1]: Reverse the entire array

First, open the indexing_practice.py file in the editor. Then, replace its entire content with the following code. This code creates a 1D array and demonstrates how to access a single element and a slice of elements.

import numpy as np

## Create a 1D array with numbers from 0 to 9
x = np.arange(10)
print("Original array:", x)

## Access a single element at index 2
element = x[2]
print("Element at index 2:", element)

## Slice the array from index 1 up to (but not including) index 7, with a step of 2
a_slice = x[1:7:2]
print("Slice from 1 to 7 with step 2:", a_slice)

After you have added the code to indexing_practice.py, save the file. Now, run the script from the terminal by executing the following command:

python indexing_practice.py

You should see the following output, which shows the original array, the element at the specified index, and the resulting slice.

Original array: [0 1 2 3 4 5 6 7 8 9]
Element at index 2: 2
Slice from 1 to 7 with step 2: [1 3 5]

Indexing Multidimensional Arrays

Now let's work with arrays that have more than one dimension. This is where NumPy really shines compared to Python lists!

Thinking in Multiple Dimensions

2D Arrays as Tables

A 2D array is like a spreadsheet or table:

  • Rows are the first dimension (horizontal)
  • Columns are the second dimension (vertical)
  • You specify both row and column indices: array[row, column]

Visualizing 2D Indexing

For a 2D array:

array = [[10, 20, 30],
         [40, 50, 60],
         [70, 80, 90]]

Indices:     0,0  0,1  0,2
             1,0  1,1  1,2
             2,0  2,1  2,2
  • array[0, 0] → 10 (first row, first column)
  • array[1, 2] → 60 (second row, third column)
  • array[2, 1] → 80 (third row, second column)

Selecting Entire Rows or Columns

  • array[0] or array[0, :] → entire first row [10, 20, 30]
  • array[:, 1] → entire second column [20, 50, 80]
  • This is much more convenient than nested Python lists!

Let's practice this with a two-dimensional (2D) array. Update your indexing_practice.py file with the code below. This script creates a 3x4 array and shows how to access a single element and an entire row.

import numpy as np

## Create a 2D array (3 rows, 4 columns)
x = np.arange(12).reshape(3, 4)
print("Original 2D array:\n", x)

## Access the element at row 1, column 2
element = x[1, 2]
print("\nElement at (1, 2):", element)

## Access the entire first row (row index 0)
first_row = x[0]
print("\nFirst row:", first_row)

Save the file and run it again from the terminal:

python indexing_practice.py

The output will display the 2D array and the specific parts you selected.

Original 2D array:
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]

Element at (1, 2): 6

First row: [0 1 2 3]

Advanced Indexing

Basic slicing works well for contiguous regions, but sometimes you need more complex selections. NumPy provides two powerful advanced indexing techniques:

Integer Array Indexing

Select arbitrary elements by providing an array of indices. This is like picking specific items from a list using their positions.

Real-world example: You have test scores and want to check scores for students at positions 3, 7, and 12:

scores = np.array([85, 92, 78, 95, 88, 76, 91, 89, 84, 93, 87, 90, 82])
student_positions = [3, 7, 12]  ## Students you're interested in
selected_scores = scores[student_positions]  ## [95, 89, 82]

Boolean Array Indexing (Masking)

Select elements based on conditions. Create a "mask" of True/False values, then use it to filter the array.

Real-world example: Filter passing grades (≥ 80) from a class:

scores = np.array([85, 92, 78, 95, 88, 76, 91, 89, 84, 93])
passing_mask = scores >= 80  ## [True, True, False, True, True, False, True, True, True, True]
passing_scores = scores[passing_mask]  ## [85, 92, 95, 88, 91, 89, 84, 93]

Why This Matters

  • Integer indexing: Perfect for sampling specific data points
  • Boolean indexing: Ideal for data filtering and conditional selections
  • Both create copies (not views), so modifications don't affect the original array

Let's try both. Replace the content of indexing_practice.py with the following code:

import numpy as np

## --- Integer Array Indexing ---
x = np.arange(10, 0, -1)
print("Array for integer indexing:", x)

## Select elements at indices 3, 3, 1, and 8
selected_elements = x[np.array([3, 3, 1, 8])]
print("Selected elements with integer array:", selected_elements)


## --- Boolean Array Indexing ---
y = np.array([1., -1., -2., 3.])
print("\nArray for boolean indexing:", y)

## Create a boolean mask for negative elements
mask = y < 0
print("Boolean mask (y < 0):", mask)

## Select elements where the condition is True
negative_elements = y[mask]
print("Elements where y < 0:", negative_elements)

Save the file and run the script:

python indexing_practice.py

Your output should demonstrate how both integer and boolean indexing work to select specific data from your arrays.

Array for integer indexing: [10  9  8  7  6  5  4  3  2  1]
Selected elements with integer array: [7 7 9 2]

Array for boolean indexing: [ 1. -1. -2.  3.]
Boolean mask (y < 0): [False  True  True False]
Elements where y < 0: [-1. -2.]

Assigning Values to Indexed Arrays

Indexing isn't just for reading data - it's also powerful for modifying data. You can use any indexing method on the left side of the assignment operator (=) to change specific elements.

Broadcasting: Making Shapes Compatible

When assigning values to indexed arrays, NumPy uses broadcasting to make the shapes compatible. This is one of NumPy's most powerful features!

Broadcasting Rules

NumPy can automatically expand smaller arrays to match larger ones during assignment, following these rules:

  1. Single value to multiple elements: One value can be assigned to many positions
  2. Small array to larger selection: As long as dimensions are compatible

Examples of Broadcasting in Assignment

## Single value to a slice
arr = np.array([1, 2, 3, 4, 5])
arr[1:4] = 99  ## [1, 99, 99, 99, 5]

## Array to matching slice
arr = np.array([1, 2, 3, 4, 5])
arr[1:4] = [10, 20, 30]  ## [1, 10, 20, 30, 5]

## Boolean indexing with broadcasting
arr = np.array([1, 2, 3, 4, 5])
arr[arr % 2 == 0] = -1  ## Replace all even numbers with -1

Important Notes

  • Broadcasting only works when shapes are compatible
  • The assigned value's shape must be able to "fit" into the indexed selection
  • This is much more efficient than looping through elements manually

Update your indexing_practice.py file with the following code to see this in action.

import numpy as np

## --- Assigning a single value to a slice ---
x = np.arange(10)
print("Original array:", x)

## Assign the value 99 to elements from index 2 to 4
x[2:5] = 99
print("After assigning 99 to slice [2:5]:", x)


## --- Assigning values based on a boolean condition ---
y = np.arange(10)
print("\nOriginal array:", y)

## Assign the value -1 to all even numbers
y[y % 2 == 0] = -1
print("After assigning -1 to even numbers:", y)

Save the file and run it from the terminal:

python indexing_practice.py

The output will show the arrays before and after the modification, demonstrating how powerful this feature is for data manipulation.

Original array: [0 1 2 3 4 5 6 7 8 9]
After assigning 99 to slice [2:5]: [ 0  1 99 99 99  5  6  7  8  9]

Original array: [0 1 2 3 4 5 6 7 8 9]
After assigning -1 to even numbers: [-1  1 -1  3 -1  5 -1  7 -1  9]

Summary

In this lab, you have learned the essential techniques for indexing NumPy arrays. You started with basic single-element access and slicing in 1D arrays, similar to Python lists. You then progressed to indexing multidimensional arrays to select specific elements and sub-arrays. Finally, you explored advanced indexing using integer and boolean arrays, and you learned how to use these powerful selection methods to modify data within an array. These skills are fundamental for effective data manipulation and analysis in Python with NumPy.