Python for Data Science & Automation · Module 2: Numerical Computing with NumPy · Lesson 8 of 34

2.3 NumPy Array Indexing & Slicing

NumPy Array Indexing & Slicing

Once a NumPy array has been created, we often need to access specific values, select rows or columns, extract a portion of the dataset, or filter values according to a condition.

NumPy provides powerful indexing and slicing techniques for these tasks.

In this lesson, you will learn:
  • What indexing means in NumPy
  • Positive and negative indexing
  • Indexing 1D arrays
  • Slicing 1D arrays
  • Using start, stop and step
  • Indexing 2D arrays
  • Selecting rows and columns
  • 2D slicing
  • Boolean indexing
  • Conditional filtering
  • Combining multiple conditions
  • Fancy indexing with integer arrays
  • Views and copies
  • Practical Data Science examples

1. What Is Indexing?

Indexing means accessing a specific element of an array using its position.

NumPy uses zero-based indexing.

Therefore, the first element has index 0, the second has index 1, and so on.

import numpy as np

numbers = np.array([10, 20, 30, 40, 50])

print(numbers[0])
print(numbers[1])
print(numbers[4])

Output:

10
20
50
Exam Tip:

NumPy indexing starts from 0, not 1.

2. Understanding Index Positions

numbers = np.array([10, 20, 30, 40, 50])

The positions are:

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

3. Negative Indexing

NumPy also supports negative indices. Negative indexing starts from the end of the array.

numbers = np.array([10, 20, 30, 40, 50])

print(numbers[-1])
print(numbers[-2])
print(numbers[-5])

Output:

50
40
10

The index positions can be visualized as:

Value:     10    20    30    40    50
Positive:   0     1     2     3     4
Negative:  -5    -4    -3    -2    -1
Remember:

-1 always refers to the last element, -2 to the second-last element, and so on.

4. Invalid Index

Attempting to access an index outside the valid range raises an IndexError.

numbers = np.array([10, 20, 30])

print(numbers[5])

The array has valid positive indices:

0, 1, 2

Therefore, index 5 is invalid.

Common Mistake:

For an array containing n elements, the largest positive index is n - 1.

5. What Is Slicing?

Slicing extracts a sequence or portion of an array.

Syntax

array[start:stop:step]
Part Meaning
start Starting index; included.
stop Ending boundary; normally excluded.
step Distance between selected elements.

6. Basic 1D Slicing

numbers = np.array([10, 20, 30, 40, 50])

print(numbers[1:4])

Output:

[20 30 40]

Indices 1, 2 and 3 are selected. Index 4 is excluded.

Exam Rule:

NumPy slicing follows the same basic start-inclusive, stop-exclusive convention used by Python sequences.

7. Omitting the Start Index

If start is omitted, slicing begins from the first element.

numbers = np.array([10, 20, 30, 40, 50])

print(numbers[:3])

Output:

[10 20 30]

8. Omitting the Stop Index

If stop is omitted, slicing continues to the end.

numbers = np.array([10, 20, 30, 40, 50])

print(numbers[2:])

Output:

[30 40 50]

9. Copying with a Full Slice

numbers = np.array([10, 20, 30, 40, 50])

result = numbers[:]

print(result)

Output:

[10 20 30 40 50]

Note that numbers[:] is a slice and generally produces a view rather than an independent copy.

10. Slicing with Step

numbers = np.array([10, 20, 30, 40, 50, 60])

print(numbers[::2])

Output:

[10 30 50]

The step value 2 selects every second element.

11. More Slicing Examples

numbers = np.array([10, 20, 30, 40, 50, 60, 70])

print(numbers[1:6:2])
print(numbers[::3])

Output:

[20 40 60]
[10 40 70]

12. Reversing an Array

A negative step can be used to traverse an array in reverse.

numbers = np.array([10, 20, 30, 40, 50])

print(numbers[::-1])

Output:

[50 40 30 20 10]
Quick Trick:

array[::-1] is a common way to reverse a NumPy array.

13. Indexing a 2D Array

A two-dimensional array contains rows and columns.

matrix = np.array([
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
])

The structure is:

         Column
          0   1   2

Row 0    10  20  30
Row 1    40  50  60
Row 2    70  80  90

An individual element can be accessed using:

array[row, column]

14. Accessing Individual 2D Elements

matrix = np.array([
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
])

print(matrix[0, 0])
print(matrix[1, 2])
print(matrix[2, 1])

Output:

10
60
80

For example:

matrix[1, 2]

means:

  • Row index = 1
  • Column index = 2
  • Value = 60

15. Alternative 2D Indexing Syntax

NumPy also supports nested indexing.

matrix[1][2]

This accesses the same element as:

matrix[1, 2]
Preferred Style:

matrix[row, column] is generally preferred for NumPy because it directly expresses multidimensional indexing.

16. Selecting a Complete Row

matrix = np.array([
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
])

print(matrix[1, :])

Output:

[40 50 60]

The colon means all columns.

The following is also commonly used:

print(matrix[1])

17. Selecting a Complete Column

print(matrix[:, 1])

Output:

[20 50 80]

Here:

  • : means all rows.
  • 1 selects column index 1.
Remember:

matrix[row, column]

matrix[:, column] → complete column

matrix[row, :] → complete row

18. Slicing a 2D Array

A two-dimensional slice can select both rows and columns.

Syntax

array[row_start:row_stop, column_start:column_stop]
matrix = np.array([
    [10, 20, 30, 40],
    [50, 60, 70, 80],
    [90, 100, 110, 120]
])

result = matrix[0:2, 1:3]

print(result)

Output:

[[20 30]
 [60 70]]

19. Selecting Multiple Rows

matrix = np.array([
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
])

print(matrix[0:2, :])

Output:

[[10 20 30]
 [40 50 60]]

20. Selecting Multiple Columns

print(matrix[:, 0:2])

Output:

[[10 20]
 [40 50]
 [70 80]]

21. 2D Slicing with Step

matrix = np.arange(1, 17).reshape(4, 4)

print(matrix[::2, ::2])

Output:

[[ 1  3]
 [ 9 11]]

Here:

  • ::2 selects every second row.
  • ::2 selects every second column.

22. Negative Indexing in 2D Arrays

matrix = np.array([
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
])

print(matrix[-1, -1])
print(matrix[-1, :])
print(matrix[:, -1])

Output:

90
[70 80 90]
[30 60 90]

Negative indexing is particularly useful when working with the last row or last column.

23. Boolean Indexing

Boolean indexing selects elements according to a Boolean condition.

numbers = np.array([10, 25, 30, 45, 50])

result = numbers > 30

print(result)

Output:

[False False False  True  True]

The Boolean array can then be used to select the matching values.

print(numbers[numbers > 30])

Output:

[45 50]

24. Filtering Data Using Conditions

Boolean indexing is extremely important in Data Science because it allows us to filter datasets.

scores = np.array([
    45, 67, 82, 91, 56, 74, 39
])

passed = scores[scores >= 50]

print(passed)

Output:

[67 82 91 56 74]

Only scores greater than or equal to 50 are selected.

25. Combining Boolean Conditions

NumPy supports combining conditions using:

  • & for AND
  • | for OR
  • ~ for NOT

AND Condition

scores = np.array([
    45, 55, 65, 75, 85, 95
])

result = scores[
    (scores >= 60) & (scores <= 90)
]

print(result)

Output:

[65 75 85]
Important:

Use &, |, and ~ for element-wise Boolean operations on NumPy arrays. Do not use Python's and and or for this purpose.

26. OR Condition

scores = np.array([
    35, 45, 55, 65, 75, 85
])

result = scores[
    (scores < 40) | (scores > 80)
]

print(result)

Output:

[35 85]

27. NOT Condition

scores = np.array([
    35, 45, 55, 65, 75
])

result = scores[~(scores < 50)]

print(result)

Output:

[55 65 75]

28. np.where() for Conditional Selection

np.where() can be used to find positions where a condition is true or to select values based on a condition.

Finding Positions

numbers = np.array([10, 25, 30, 45, 50])

positions = np.where(numbers > 30)

print(positions)

The result identifies the indices where the condition is true.

Conditional Values

numbers = np.array([10, 25, 30, 45, 50])

result = np.where(
    numbers >= 30,
    "Pass",
    "Fail"
)

print(result)

Output:

['Fail' 'Fail' 'Pass' 'Pass' 'Pass']

29. Fancy Indexing

Fancy indexing means selecting elements using an array or list of integer indices.

numbers = np.array([
    10, 20, 30, 40, 50
])

indices = [0, 2, 4]

result = numbers[indices]

print(result)

Output:

[10 30 50]

This allows non-contiguous elements to be selected in a single operation.

30. Fancy Indexing in 2D Arrays

matrix = np.array([
    [10, 20],
    [30, 40],
    [50, 60],
    [70, 80]
])

rows = [0, 2, 3]

result = matrix[rows]

print(result)

Output:

[[10 20]
 [50 60]
 [70 80]]

The selected rows are 0, 2 and 3.

31. Selecting Specific 2D Coordinates

Integer arrays can be used to select corresponding row and column positions.

matrix = np.array([
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
])

rows = [0, 1, 2]
columns = [2, 1, 0]

result = matrix[rows, columns]

print(result)

Output:

[30 50 70]

The selected coordinates are:

(0, 2)
(1, 1)
(2, 0)

32. Indexing vs Slicing vs Boolean Indexing

Technique Purpose Example
Indexing Access a specific element. arr[2]
Slicing Select a continuous/rule-based range. arr[1:5]
Boolean indexing Select values satisfying a condition. arr[arr > 50]
Fancy indexing Select specified positions. arr[[0, 2, 4]]

33. Views and Copies

An important concept in NumPy is the difference between a view and a copy.

A view refers to the same underlying data, while a copy contains independent data.

Slice Example

numbers = np.array([10, 20, 30, 40, 50])

part = numbers[1:4]

part[0] = 999

print(numbers)

Output:

[ 10 999  30  40  50]

The original array changed because a basic slice generally returns a view.

34. Creating an Independent Copy

Use copy() when you want an independent array.

numbers = np.array([10, 20, 30, 40, 50])

part = numbers[1:4].copy()

part[0] = 999

print(numbers)
print(part)

Output:

[10 20 30 40 50]
[999  30  40]
Best Practice:

Use .copy() when you intentionally need an independent array and do not want changes to affect the original data.

35. Boolean Mask

A Boolean mask is an array of True and False values used to filter data.

temperatures = np.array([
    18, 22, 27, 31, 35
])

mask = temperatures > 25

print(mask)

Output:

[False False  True  True  True]

Applying the mask:

print(temperatures[mask])

Output:

[27 31 35]

36. Data Science Example — Filtering Student Marks

marks = np.array([
    45, 78, 91, 56, 88, 39, 72
])

high_scores = marks[marks >= 75]

print(high_scores)

Output:

[78 91 88]

This is a basic example of filtering numerical observations based on a business or analytical rule.

37. Filtering a 2D Array

marks = np.array([
    [78, 65, 91],
    [55, 88, 72],
    [92, 81, 69]
])

result = marks[marks >= 80]

print(result)

Output:

[91 88 92 81]

Boolean indexing returns the matching elements as a one-dimensional result.

38. Selecting Rows Using a Condition

Suppose each row represents a student and the columns represent different subjects.

marks = np.array([
    [78, 82, 91],
    [55, 60, 65],
    [88, 92, 95],
    [45, 51, 58]
])

row_condition = marks[:, 0] >= 70

selected = marks[row_condition]

print(selected)

Output:

[[78 82 91]
 [88 92 95]]

Here, only rows where the first subject score is at least 70 are selected.

39. Selecting Rows Using Multiple Conditions

marks = np.array([
    [78, 82, 91],
    [55, 60, 65],
    [88, 92, 95],
    [45, 51, 58]
])

condition = (
    (marks[:, 0] >= 70) &
    (marks[:, 1] >= 80)
)

selected = marks[condition]

print(selected)

Output:

[[78 82 91]
 [88 92 95]]

40. Conditional Selection from Columns

marks = np.array([
    [78, 82, 91],
    [55, 60, 65],
    [88, 92, 95]
])

math_marks = marks[:, 0]

high_math = math_marks[math_marks >= 80]

print(high_math)

Output:

[88]

41. np.clip() — Restricting Values to a Range

np.clip() is useful when values must be constrained within a specified minimum and maximum.

values = np.array([
    10, 25, 50, 75, 100
])

result = np.clip(
    values,
    20,
    80
)

print(result)

Output:

[20 25 50 75 80]

Values below 20 become 20, while values above 80 become 80.

42. np.nonzero()

np.nonzero() returns the indices of non-zero elements.

values = np.array([0, 5, 0, 8, 12])

positions = np.nonzero(values)

print(positions)

The returned indices identify the positions containing non-zero values.

43. np.argwhere()

np.argwhere() returns the indices of elements that satisfy a condition.

values = np.array([10, 25, 30, 45, 50])

positions = np.argwhere(values > 30)

print(positions)

The result contains the positions where the condition is true.

44. Indexing & Slicing Cheatsheet

Operation Syntax Purpose
Single element arr[2] Access one element.
Last element arr[-1] Access the final element.
Basic slice arr[1:5] Select a range.
Every second value arr[::2] Select with step 2.
Reverse arr[::-1] Reverse an array.
2D element arr[1, 2] Select row 1, column 2.
Complete row arr[1, :] Select one row.
Complete column arr[:, 1] Select one column.
2D slice arr[0:2, 1:3] Select rows and columns.
Boolean filter arr[arr > 50] Select values satisfying a condition.
Fancy indexing arr[[0, 2, 4]] Select specified positions.

45. NumPy Interview Questions

Q1. What is the difference between indexing and slicing?

View Answer

Indexing normally accesses a specific element, whereas slicing selects a range or structured portion of an array.

Q2. Does NumPy use zero-based indexing?

View Answer

Yes. The first element has index 0.

Q3. What does arr[::-1] do?

View Answer

It returns the elements in reverse order using a step of -1.

Q4. How do you select the second column of a 2D NumPy array?

View Answer

Use arr[:, 1]. The colon selects all rows and index 1 selects the second column.

Q5. What is Boolean indexing?

View Answer

Boolean indexing uses an array of Boolean values or a condition to select elements that satisfy a particular criterion.

Q6. Why are parentheses important when combining NumPy conditions?

View Answer

Each comparison should normally be enclosed in parentheses when using element-wise operators such as & and |, so that the comparisons are evaluated as intended.

Q7. What is fancy indexing?

View Answer

Fancy indexing selects elements using arrays or lists of integer indices, allowing non-contiguous positions to be selected.

Q8. Why might you use copy() after slicing?

View Answer

Basic slices generally produce views. Using copy() creates independent data so changes to the new array do not modify the original.

46. Examination Questions

Multiple Choice Questions

Q1. What is the index of the first element of a NumPy array?

  1. 1
  2. -1
  3. 0
  4. None

Answer: C — 0

Q2. What is the output?

arr = np.array([10, 20, 30, 40, 50])

print(arr[1:4])
  1. [10 20 30]
  2. [20 30 40]
  3. [20 30 40 50]
  4. [10 20 30 40]

Answer: B — [20 30 40]

Q3. What does arr[-1] return?

  1. First element
  2. Second element
  3. Last element
  4. Array length

Answer: C — Last element

Q4. Which expression selects the second column of a 2D array?

  1. arr[1, :]
  2. arr[:, 1]
  3. arr[1]
  4. arr[:, 2]

Answer: B — arr[:, 1]

Q5. What does the following expression select?

arr[arr > 50]
  1. All elements
  2. Elements less than 50
  3. Elements greater than 50
  4. Only element at index 50

Answer: C — Elements greater than 50

Q6. Which operator is used for element-wise AND between NumPy Boolean conditions?

  1. and
  2. &&
  3. &
  4. AND

Answer: C — &

Short Answer Questions

  1. Explain zero-based indexing with an example.
  2. Explain negative indexing in NumPy.
  3. Explain the syntax start:stop:step.
  4. How can you select an entire row from a 2D NumPy array?
  5. How can you select an entire column from a 2D NumPy array?
  6. What is Boolean indexing? Give an example.
  7. Differentiate between Boolean indexing and fancy indexing.
  8. Why should parentheses be used around individual conditions when using & or |?

47. Practical Challenge

Build a NumPy Data Filter

Create a NumPy-based program that analyzes a collection of student marks.

  1. Create a 1D array containing at least 10 marks.
  2. Display the first, last, and third elements.
  3. Display the first five marks using slicing.
  4. Display every second mark.
  5. Display the marks in reverse order.
  6. Select all marks greater than or equal to 75.
  7. Select marks between 50 and 80.
  8. Find the positions of marks greater than 90 using np.where().
  9. Create a 2D array and display one complete row.
  10. Display one complete column.
  11. Extract a rectangular portion of the 2D array using slicing.
  12. Select specific rows using fancy indexing.
  13. Create a copy of a slice and demonstrate that modifying the copy does not modify the original.

48. Common Mistakes to Avoid

  1. Forgetting zero-based indexing: The first element is at index 0.
  2. Including the stop index: In arr[1:4], index 4 is excluded.
  3. Using and instead of &: Use element-wise Boolean operators for NumPy arrays.
  4. Forgetting parentheses: Write (arr > 10) & (arr < 50).
  5. Confusing rows and columns: In arr[row, column], the first index is the row.
  6. Assuming slices are always independent: Basic slices generally return views.
  7. Modifying a slice unintentionally: Use .copy() when an independent array is required.

49. Key Takeaways

  • NumPy uses zero-based indexing.
  • Negative indices access elements from the end of the array.
  • Basic slicing follows the pattern start:stop:step.
  • The start position is included, while the stop position is normally excluded.
  • arr[::-1] reverses a one-dimensional array.
  • 2D elements can be accessed using arr[row, column].
  • arr[row, :] selects a complete row.
  • arr[:, column] selects a complete column.
  • Boolean indexing is used for conditional filtering.
  • &, |, and ~ are used for element-wise Boolean operations.
  • np.where() can locate positions or select values based on a condition.
  • Fancy indexing selects specified positions using integer index arrays.
  • Basic slices generally return views, while copy() creates independent data.
Golden Rule:

Indexing selects specific data, slicing selects a structured portion, and Boolean/fancy indexing lets you select data based on conditions or specified positions.