Python · Lesson 25

NumPy Deep Dive: Data Types, Copy vs View, Shape, Reshape, Iterating, and Joining Arrays

10 phases  ·  Build: Stage 1 · Setup and Data Inspection

👋 Welcome to Lesson 25

Welcome to Lesson 25! By now you know how to create NumPy arrays and access elements inside them. In this lesson, we go deeper. We will learn:

  • What data types are and how NumPy manages them
  • The difference between copying and viewing an array (a concept that surprises even experienced programmers)
  • How to understand and inspect the shape of an array
  • How to reshape arrays from one structure into another
  • How to iterate (loop) through arrays of any number of dimensions
  • How to join multiple arrays together into one

Each concept builds on the last. By the end, you will be able to manipulate arrays like a data science professional.

Real-world relevance: In machine learning, you constantly reshape arrays to feed them into models. In data analysis, you join datasets together. In scientific computing, you iterate through large arrays to process measurements. Everything in this lesson is used daily by professional data scientists and engineers.


📚 10 phases🏗️ Stage 1 · Setup and Data Inspection🐍 GitHub Repo
Phase 1 of 10
Lesson Introduction

Welcome to Lesson 25! By now you know how to create NumPy arrays and access elements inside them. In this lesson, we go deeper. We will learn:

  • What data types are and how NumPy manages them
  • The difference between copying and viewing an array (a concept that surprises even experienced programmers)
  • How to understand and inspect the shape of an array
  • How to reshape arrays from one structure into another
  • How to iterate (loop) through arrays of any number of dimensions
  • How to join multiple arrays together into one

Each concept builds on the last. By the end, you will be able to manipulate arrays like a data science professional.

Real-world relevance: In machine learning, you constantly reshape arrays to feed them into models. In data analysis, you join datasets together. In scientific computing, you iterate through large arrays to process measurements. Everything in this lesson is used daily by professional data scientists and engineers.


✏️ Your Task
Practise what you just learned about Lesson Introduction. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 2 of 10
Prerequisites · What You Need to Know First

Before we begin, let us quickly review some ideas we depend on:

What is a NumPy array? A NumPy array is an ordered list of values stored in a grid. Every element in the array must be of the same type (like all integers, or all floats). This is different from a Python list, where you can mix types.

What is a dimension? A dimension (also called an axis) is a direction in which data is arranged.

  • A flat list like [1, 2, 3] is 1-dimensional (1D).
  • A table like [[1,2],[3,4]] is 2-dimensional (2D) · it has rows and columns.
  • A cube like [[[1,2],[3,4]],[[5,6],[7,8]]] is 3-dimensional (3D).

What is a data type? A data type defines the kind of value being stored · for example, a whole number (integer), a decimal number (float), or text (string).


✏️ Your Task
Practise what you just learned about Prerequisites · What You Need to Know First. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 3 of 10
Part 1 · NumPy Data Types

1.1 What Is a Data Type, and Why Does It Matter?

Imagine you are filling out a form. Some fields only accept whole numbers (your age), some accept text (your name), and some accept decimals (your weight). A data type is exactly that · a rule about what kind of value a slot can hold.

In Python, you have basic built-in types like int, float, str, and bool. NumPy adds its own more precise types, because when you are working with millions of numbers, using the right type saves memory and makes calculations faster.

Think of it this way: storing the number 5 as a 64-bit integer uses 8 bytes of memory. If you have one billion numbers and use the wrong type, you could waste gigabytes of RAM unnecessarily.

1.2 Python's Built-in Data Types (Quick Review)

TypeWhat it holdsExample
strText"Hello"
intWhole numbers-3, 0, 42
floatDecimal numbers3.14, -0.5
boolTrue or FalseTrue, False
complexComplex numbers1.0 + 2.0j

1.3 NumPy's Data Type System

NumPy uses short one-character codes to refer to types. Here is the full list:

CodeFull NameWhat it Means
iintegerWhole numbers (positive and negative)
bbooleanTrue or False
uunsigned integerWhole numbers (positive only, no negatives)
ffloatNumbers with decimals
ccomplex floatComplex numbers with real and imaginary parts
mtimedeltaA difference between two times
MdatetimeA specific date and time
OobjectAny Python object
Sstring (bytes)Fixed-length byte strings
Uunicode stringText (Unicode characters)
VvoidFixed chunk of memory for other types

Key insight: NumPy stores data much more efficiently than Python lists because every element in a NumPy array has the exact same type. A list can hold a mix; a NumPy array cannot.

1.4 Checking the Data Type of an Array

Every NumPy array has a property called .dtype that tells you the type of its elements.

What does .dtype mean? It stands for "data type." It is like asking: "What kind of values are stored inside this array?"

Example 1 · Integer array:

python
import numpy as np

arr = np.array([1, 2, 3, 4])

print(arr.dtype)

Expected Output:

code
int64

Line-by-line explanation:

  • import numpy as np · brings in the NumPy library and gives it the short name np
  • arr = np.array([1, 2, 3, 4]) · creates a NumPy array from a Python list of whole numbers
  • print(arr.dtype) · prints the data type. int64 means a 64-bit integer (a whole number using 64 bits of memory)

Example 2 · String array:

python
import numpy as np

arr = np.array(['apple', 'banana', 'cherry'])

print(arr.dtype)

Expected Output:

code
<U6

Explanation of <U6:

  • U means Unicode string
  • 6 means the longest string in the array has 6 characters ("banana" and "cherry" both have 6)
  • < means little-endian byte order (a low-level storage detail you can ignore for now)

Thinking prompt: What do you think would happen if you added a longer string like "watermelon" to this array? How would the dtype change?

1.5 Creating an Array With a Specific Data Type

You can tell NumPy exactly what data type to use when creating an array. You do this with the dtype parameter inside np.array().

What is a parameter? A parameter is an optional instruction you give to a function to customize how it works.

Example · Create a string array from numbers:

python
import numpy as np

arr = np.array([1, 2, 3, 4], dtype='S')

print(arr)
print(arr.dtype)

Expected Output:

code
[b'1' b'2' b'3' b'4']
|S1

Explanation:

  • dtype='S' · tells NumPy to store these values as byte strings
  • The b prefix in b'1' means "bytes literal" · it is a string stored as raw bytes
  • |S1 means each element takes up 1 byte (since '1', '2', etc. are each 1 character)

Example · Create a 4-byte integer array:

python
import numpy as np

arr = np.array([1, 2, 3, 4], dtype='i4')

print(arr)
print(arr.dtype)

Expected Output:

code
[1 2 3 4]
int32

Explanation of 'i4':

  • i = integer
  • 4 = 4 bytes (32 bits)
  • So i4 creates 32-bit integers, also known as int32

For i, u, f, S, and U, you can specify a byte size like i4, f8, S10, etc.

1.6 What Happens When Conversion Fails?

If you try to force NumPy to convert values that cannot be converted, it raises a ValueError.

What is a ValueError? It is Python's way of saying: "The type of value you gave me makes no sense for this operation."

Example · Trying to convert non-numeric text to integer:

python
import numpy as np

arr = np.array(['a', '2', '3'], dtype='i')

Expected Output (Error):

code
ValueError: invalid literal for int() with base 10: 'a'

Why? The letter 'a' cannot be converted to a number. '2' and '3' could be, but 'a' cannot · so the whole operation fails.

Beginner mistake warning: You might think '2' will become 2 and 'a' will just become 0. It will NOT. NumPy fails completely if even one element cannot be converted.

1.7 Converting an Array to a Different Data Type (astype)

What if you already have an array and want to change its type? You cannot change it in-place (directly). Instead, you create a copy of the array with the new type using the .astype() method.

What does "in-place" mean? It means changing the actual original thing, rather than making a new version. NumPy requires you to make a new array when changing types.

Example 1 · Convert float to integer using 'i':

python
import numpy as np

arr = np.array([1.1, 2.1, 3.1])

newarr = arr.astype('i')

print(newarr)
print(newarr.dtype)

Expected Output:

code
[1 2 3]
int32

Explanation:

  • arr = np.array([1.1, 2.1, 3.1]) · creates a float array
  • newarr = arr.astype('i') · creates a NEW array where each value is converted to integer. The decimal part is simply cut off (truncated), not rounded
  • 1.1 becomes 1, 2.1 becomes 2, 3.1 becomes 3

Thinking prompt: What do you think 3.9 would become? Try it! (Answer: 3 · it is truncated, not rounded)

Example 2 · Convert float to integer using Python's built-in int:

python
import numpy as np

arr = np.array([1.1, 2.1, 3.1])

newarr = arr.astype(int)

print(newarr)
print(newarr.dtype)

Expected Output:

code
[1 2 3]
int64

Both 'i' and int work, but they may produce slightly different integer sizes.

Example 3 · Convert integer to boolean:

python
import numpy as np

arr = np.array([1, 0, 3])

newarr = arr.astype(bool)

print(newarr)
print(newarr.dtype)

Expected Output:

code
[ True False  True]
bool

Explanation: In Python (and NumPy), any non-zero number converts to True, and 0 converts to False. This is called "truthiness."

  • 1True
  • 0False
  • 3True

Real-world use: Converting to boolean is very common in data analysis. For example, you might have a column of scores and want to mark which ones are passing (non-zero = True) vs failing (zero = False).


✏️ Your Task
Practise what you just learned about NumPy Data Types. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 4 of 10
Part 2 · Copy vs View

2.1 The Big Idea: What Are We Really Talking About?

This is one of the most important · and most misunderstood · concepts in NumPy. Let us start with a real-world analogy.

Analogy · The Whiteboard and the Photo:

Imagine a whiteboard in your classroom with some numbers written on it. Now consider two different actions:

  1. Taking a photo of the whiteboard · The photo shows the numbers, but if someone erases the whiteboard and writes new numbers, your photo still shows the old numbers. They are separate.
  1. Looking at the whiteboard through a window · You can see the current numbers. If someone erases them and writes new ones, you immediately see the new numbers through the window.

In NumPy:

  • Copy = the photo (completely independent)
  • View = the window (connected to the original)

2.2 What Is a Copy?

A copy is a brand-new, completely separate array. It takes the values from the original and stores them in a new location in memory. Changes to the copy do not affect the original. Changes to the original do not affect the copy.

Example · Make a copy, change the original:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5])
x = arr.copy()
arr[0] = 42

print(arr)
print(x)

Expected Output:

code
[42  2  3  4  5]
[1 2 3 4 5]

Line-by-line explanation:

  • arr = np.array([1, 2, 3, 4, 5]) · creates the original array
  • x = arr.copy() · creates a brand-new separate array x with the same values
  • arr[0] = 42 · changes the first element of arr to 42
  • print(arr) · shows [42 2 3 4 5] (the original changed)
  • print(x) · shows [1 2 3 4 5] (the copy is unchanged · they are independent)

The copy SHOULD NOT be affected by changes to the original.

2.3 What Is a View?

A view is like a window into the same data. It does not create a new copy of the data. Both the view and the original point to the same memory. If you change one, the other changes too.

Example 1 · Make a view, change the original:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5])
x = arr.view()
arr[0] = 42

print(arr)
print(x)

Expected Output:

code
[42  2  3  4  5]
[42  2  3  4  5]

Explanation: arr[0] was set to 42. Both arr and x show 42 because they share the same underlying data.

The view SHOULD be affected by changes to the original.

Example 2 · Make a view, change the VIEW:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5])
x = arr.view()
x[0] = 31

print(arr)
print(x)

Expected Output:

code
[31  2  3  4  5]
[31  2  3  4  5]

Explanation: This time we changed x[0] (the view). Because the view and the original share the same memory, the original arr also changed.

Common beginner mistake: Many beginners accidentally modify data they wanted to keep. They create a view thinking it is a copy, then modify it, and are surprised when the original changes too. Always use .copy() when you want an independent copy.

2.4 How to Check: Does This Array Own Its Data?

NumPy gives every array a special attribute called .base. This attribute tells you whether the array owns its own data or is borrowing it from somewhere else.

  • If .base returns None → the array owns its data (it is a copy or the original)
  • If .base returns another array → the array is a view of that other array

Example:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5])

x = arr.copy()
y = arr.view()

print(x.base)
print(y.base)

Expected Output:

code
None
[1 2 3 4 5]

Explanation:

  • x.base is None · x is a copy; it owns its own data
  • y.base is [1 2 3 4 5] · y is a view; it points back to arr

Quick mental test: Before printing .base, ask yourself: "Is this a copy or a view?" Then check. This builds your intuition.


✏️ Your Task
Practise what you just learned about Copy vs View. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 5 of 10
Part 3 · Array Shape

3.1 What Is "Shape"?

The shape of an array describes how many elements it has in each dimension. It is like describing the dimensions of a box:

  • A 1D array of 5 elements has shape (5,) · it is like a single row with 5 slots
  • A 2D array with 2 rows and 4 columns has shape (2, 4) · like a table
  • A 3D array with 2 groups, 3 rows, 4 columns has shape (2, 3, 4) · like a stack of tables

Think of shape as the answer to: "How is this data organized?"

3.2 Getting the Shape with .shape

Every NumPy array has a .shape attribute that returns a tuple describing its dimensions.

What is a tuple? A tuple is like a list, but it cannot be changed. It uses round brackets () instead of square brackets [].

Example 1 · Shape of a 2D array:

python
import numpy as np

arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])

print(arr.shape)

Expected Output:

code
(2, 4)

Explanation:

  • The outer list [[...], [...]] has 2 inner lists → 2 rows
  • Each inner list has 4 elements → 4 columns
  • Shape = (2, 4) = 2 rows, 4 columns

How to read shape tuples:

  • First number = number of rows (or "how many groups at the outermost level")
  • Second number = number of columns (or "how many elements in each group")
  • Third number (if present) = elements in the next level deeper

Example 2 · Creating an array with 5 dimensions:

python
import numpy as np

arr = np.array([1, 2, 3, 4], ndmin=5)

print(arr)
print('shape of array :', arr.shape)

Expected Output:

code
[[[[[1 2 3 4]]]]]
shape of array : (1, 1, 1, 1, 4)

Explanation of ndmin=5:

  • ndmin = minimum number of dimensions
  • We have 4 elements but we asked for 5 dimensions
  • NumPy wraps them in enough brackets to reach 5 dimensions
  • Shape (1, 1, 1, 1, 4) means: 1 group at levels 1 · 4, then 4 elements at the deepest level

Understanding the shape tuple position by position:

  • Index 0 → outermost dimension has 1 element
  • Index 1 → next dimension has 1 element
  • Index 2 → next dimension has 1 element
  • Index 3 → next dimension has 1 element
  • Index 4 → innermost dimension has 4 elements

Thinking prompt: If you have arr.shape = (3, 4, 5), how many total elements does the array contain? (Answer: 3 × 4 × 5 = 60 elements)


✏️ Your Task
Practise what you just learned about Array Shape. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 6 of 10
Part 4 · Array Reshape

4.1 What Does "Reshape" Mean?

Reshaping an array means changing how the same data is organized · rearranging the elements into a different dimensional structure · without changing the values themselves.

Think of it like rearranging 12 books:

  • You can put them in one long row: 12 books in a line (1D, shape (12,))
  • Or 3 rows of 4 books (2D, shape (3, 4))
  • Or 2 shelves, each with 2 rows of 3 books (3D, shape (2, 2, 3))

The books are the same · only the arrangement changes.

4.2 Reshape from 1D to 2D

Example · Convert 12 elements into a 4×3 table:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])

newarr = arr.reshape(4, 3)

print(newarr)

Expected Output:

code
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]

Explanation:

  • Original: 12 elements in a flat line
  • reshape(4, 3) = make 4 rows, 3 columns
  • 4 × 3 = 12 ✓ (the total must match!)
  • Elements fill in row by row: first row is 1, 2, 3; second row is 4, 5, 6; etc.

4.3 Reshape from 1D to 3D

Example · Convert 12 elements into a 3D array:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])

newarr = arr.reshape(2, 3, 2)

print(newarr)

Expected Output:

code
[[[ 1  2]
  [ 3  4]
  [ 5  6]]

 [[ 7  8]
  [ 9 10]
  [11 12]]]

Explanation:

  • reshape(2, 3, 2) = 2 outer groups, each containing 3 rows, each row containing 2 elements
  • 2 × 3 × 2 = 12 ✓
  • Think of it as 2 small tables, each with 3 rows and 2 columns

4.4 The Golden Rule of Reshaping

You can only reshape if the total number of elements stays the same.

  • 8 elements can become shape (2, 4), (4, 2), (8, 1), (1, 8), (2, 2, 2)
  • 8 elements CANNOT become shape (3, 3) because 3×3=9 ≠ 8

Example · Attempting an invalid reshape:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8])

newarr = arr.reshape(3, 3)

print(newarr)

Expected Output (Error):

code
ValueError: cannot reshape array of size 8 into shape (3,3)

Beginner mistake: Always check that rows × columns × depth... = total number of elements before reshaping.

4.5 Does Reshape Return a Copy or a View?

Example · Check what reshape returns:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8])

print(arr.reshape(2, 4).base)

Expected Output:

code
[1 2 3 4 5 6 7 8]

Explanation: The .base attribute returns the original array (not None), which means reshape() returns a view, not a copy. The reshaped array shares data with the original.

Important implication: If you modify elements of a reshaped array, the original will also change, because they share the same memory.

4.6 The Unknown Dimension: Using -1

Sometimes you know one dimension but want NumPy to figure out the other. You can pass -1 as a placeholder for one unknown dimension. NumPy will calculate the correct value automatically.

Example · Let NumPy figure out one dimension:

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8])

newarr = arr.reshape(2, 2, -1)

print(newarr)

Expected Output:

code
[[[1 2]
  [3 4]]

 [[5 6]
  [7 8]]]

Explanation:

  • We told NumPy: "Make it 2 groups of 2 rows, and figure out the rest."
  • Total = 8 elements. 2 × 2 × ? = 8. So ? = 2.
  • NumPy calculates the -1 dimension as 2.

Note: You can only use -1 for ONE dimension at a time. If you use it twice, NumPy cannot figure out two unknowns at once.

4.7 Flattening: Converting Any Array Back to 1D

Flattening means converting a multi-dimensional array into a simple 1D array. Use reshape(-1) to do this.

Example · Flatten a 2D array:

python
import numpy as np

arr = np.array([[1, 2, 3], [4, 5, 6]])

newarr = arr.reshape(-1)

print(newarr)

Expected Output:

code
[1 2 3 4 5 6]

Explanation:

  • reshape(-1) tells NumPy: "I want 1D, figure out the length."
  • The 2D array [[1,2,3],[4,5,6]] becomes [1, 2, 3, 4, 5, 6]

Real-world use: Flattening is extremely common in machine learning. Image data is often stored as 2D or 3D arrays, but many models require a flat 1D input vector.

Note: Other functions like flatten() and ravel() also flatten arrays. flatten() always returns a copy. ravel() returns a view when possible. reshape(-1) is the most flexible approach.


✏️ Your Task
Practise what you just learned about Array Reshape. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 7 of 10
Part 5 · Array Iterating

5.1 What Does "Iterating" Mean?

Iterating means going through each element of a collection, one at a time, and doing something with each one · like checking each item in a shopping cart at checkout.

In NumPy, you can iterate through arrays using Python's for loop, just like with regular Python lists. The behaviour depends on how many dimensions the array has.

5.2 Iterating a 1D Array

With a 1D array, for x in arr gives you one element at a time.

Example:

python
import numpy as np

arr = np.array([1, 2, 3])

for x in arr:
    print(x)

Expected Output:

code
1
2
3

Explanation:

  • for x in arr: · loops over each element
  • x takes the value of each element one at a time: first 1, then 2, then 3
  • print(x) · prints each value on its own line

5.3 Iterating a 2D Array

With a 2D array, for x in arr gives you one row at a time · not individual elements.

Example 1 · Iterate rows:

python
import numpy as np

arr = np.array([[1, 2, 3], [4, 5, 6]])

for x in arr:
    print(x)

Expected Output:

code
[1 2 3]
[4 5 6]

Explanation: x is each row (sub-array), not each individual number.

Example 2 · Iterate individual elements of a 2D array (nested loops):

python
import numpy as np

arr = np.array([[1, 2, 3], [4, 5, 6]])

for x in arr:
    for y in x:
        print(y)

Expected Output:

code
1
2
3
4
5
6

Explanation:

  • The outer for x in arr loops over rows
  • The inner for y in x loops over each element within each row
  • Together they reach every individual number

5.4 Iterating a 3D Array

With a 3D array, the outermost for loop gives you 2D slices (layers), not rows or individual values.

Example 1 · Iterate 2D layers:

python
import numpy as np

arr = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])

for x in arr:
    print(x)

Expected Output:

code
[[1 2 3]
 [4 5 6]]
[[ 7  8  9]
 [10 11 12]]

Example 2 · Iterate down to individual scalar values:

python
import numpy as np

arr = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])

for x in arr:
    for y in x:
        for z in y:
            print(z)

Expected Output:

code
1
2
3
4
5
6
7
8
9
10
11
12

Explanation: Three nested loops for a 3D array: one per dimension.

Pattern to remember: For an N-dimensional array, you need N nested for loops to reach each individual scalar (single value). This gets impractical for very high dimensions · which is why nditer() exists.

5.5 Smart Iteration with nditer()

nditer() is a powerful NumPy function that lets you iterate over every scalar element of an array of ANY number of dimensions, using just a single loop. It removes the need for deeply nested for loops.

Example · Iterate through a 3D array with just one loop:

python
import numpy as np

arr = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])

for x in np.nditer(arr):
    print(x)

Expected Output:

code
1
2
3
4
5
6
7
8

Explanation:

  • np.nditer(arr) automatically handles all dimensions
  • x is each individual scalar value, in order
  • No nested loops needed!

5.6 Iterating with a Different Data Type Using nditer()

You can change the data type of elements during iteration using the op_dtypes argument. But since NumPy cannot change data in-place during iteration, it needs a temporary buffer. Enable the buffer using flags=['buffered'].

What is a buffer? Think of it as a temporary scratch pad. NumPy converts each element to the new type in this temporary space, then gives it to you.

Example · Iterate and read values as strings:

python
import numpy as np

arr = np.array([1, 2, 3])

for x in np.nditer(arr, flags=['buffered'], op_dtypes=['S']):
    print(x)

Expected Output:

code
b'1'
b'2'
b'3'

Explanation:

  • op_dtypes=['S'] · convert each element to string (bytes) type
  • flags=['buffered'] · allow a buffer for temporary type conversion
  • The b prefix shows these are byte strings

5.7 Iterating with a Step (Skipping Elements)

You can iterate with a step size (skip every other element) by using array slicing inside nditer().

Example · Skip every other element in a 2D array:

python
import numpy as np

arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])

for x in np.nditer(arr[:, ::2]):
    print(x)

Expected Output:

code
1
3
5
7

Explanation:

  • arr[:, ::2] · selects all rows (:), but only every 2nd column (::2)
  • Columns selected: 0, 2 (indices 0 and 2 = values 1,3 in row 1 and 5,7 in row 2)
  • nditer() then iterates over this filtered selection

5.8 Enumerated Iteration with ndenumerate()

Sometimes you need both the index (position) and the value of each element as you iterate. ndenumerate() provides both.

What is enumeration? It is the process of attaching a sequence number or position label to each item as you loop through them.

Example 1 · Enumerate a 1D array:

python
import numpy as np

arr = np.array([1, 2, 3])

for idx, x in np.ndenumerate(arr):
    print(idx, x)

Expected Output:

code
(0,) 1
(1,) 2
(2,) 3

Explanation:

  • idx is the index (position) as a tuple · (0,) means "position 0 in dimension 0"
  • x is the value at that position

Example 2 · Enumerate a 2D array:

python
import numpy as np

arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])

for idx, x in np.ndenumerate(arr):
    print(idx, x)

Expected Output:

code
(0, 0) 1
(0, 1) 2
(0, 2) 3
(0, 3) 4
(1, 0) 5
(1, 1) 6
(1, 2) 7
(1, 3) 8

Explanation:

  • (0, 0) means row 0, column 0 → value is 1
  • (0, 1) means row 0, column 1 → value is 2
  • (1, 3) means row 1, column 3 → value is 8

Real-world use: ndenumerate() is very useful when you need to find the position of specific values in a 2D array, like finding the location of a maximum temperature in a weather grid.


✏️ Your Task
Practise what you just learned about Array Iterating. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 8 of 10
Part 6 · Joining Arrays

6.1 What Does "Joining" Mean?

Joining (also called concatenating or stacking) means combining two or more arrays into one single array. This is similar to merging spreadsheets or combining datasets.

In NumPy, you join arrays along an axis. Remember, an axis is just a direction:

  • Axis 0 = along rows (top to bottom, stacking vertically)
  • Axis 1 = along columns (left to right, combining side by side)

6.2 Joining with concatenate()

np.concatenate() is the main function for joining arrays. You pass it a tuple (or list) of arrays to combine, and optionally specify the axis.

Important: np.concatenate() requires all arrays to have the same number of dimensions and matching sizes in all axes except the one being concatenated.

Example 1 · Join two 1D arrays:

python
import numpy as np

arr1 = np.array([1, 2, 3])

arr2 = np.array([4, 5, 6])

arr = np.concatenate((arr1, arr2))

print(arr)

Expected Output:

code
[1 2 3 4 5 6]

Explanation:

  • arr1 = [1, 2, 3] and arr2 = [4, 5, 6]
  • np.concatenate((arr1, arr2)) · note the double parentheses: the outer ones are the function call, the inner ones are a tuple holding the two arrays
  • Default axis is 0. For 1D arrays, this simply joins them end-to-end
  • Result: [1 2 3 4 5 6]

Example 2 · Join two 2D arrays along columns (axis=1):

python
import numpy as np

arr1 = np.array([[1, 2], [3, 4]])

arr2 = np.array([[5, 6], [7, 8]])

arr = np.concatenate((arr1, arr2), axis=1)

print(arr)

Expected Output:

code
[[1 2 5 6]
 [3 4 7 8]]

Explanation:

  • arr1 has shape (2, 2): two rows, two columns
  • arr2 has shape (2, 2): two rows, two columns
  • axis=1 means join along columns (side by side)
  • Row 0: [1, 2] from arr1 + [5, 6] from arr2 = [1, 2, 5, 6]
  • Row 1: [3, 4] from arr1 + [7, 8] from arr2 = [3, 4, 7, 8]

Axis 0 vs Axis 1 intuition:

  • axis=0 → stack one ON TOP OF the other (add more rows)
  • axis=1 → place one BESIDE the other (add more columns)

6.3 Joining with stack()

np.stack() is similar to concatenate(), but with one important difference: stacking creates a NEW axis, while concatenating joins along an existing axis.

Analogy:

  • concatenate() = laying sheets of paper end-to-end on the same table
  • stack() = placing sheets of paper on top of each other to create a pile (a new dimension)

Example · Stack two 1D arrays along axis=1:

python
import numpy as np

arr1 = np.array([1, 2, 3])

arr2 = np.array([4, 5, 6])

arr = np.stack((arr1, arr2), axis=1)

print(arr)

Expected Output:

code
[[1 4]
 [2 5]
 [3 6]]

Explanation:

  • Both arrays have 3 elements
  • axis=1 creates a new second dimension (columns)
  • Element arr1[0]=1 and arr2[0]=4 become the first row [1, 4]
  • Element arr1[1]=2 and arr2[1]=5 become the second row [2, 5]
  • Element arr1[2]=3 and arr2[2]=6 become the third row [3, 6]
  • Result shape: (3, 2) · 3 rows, 2 columns

6.4 Convenience Stacking Functions

NumPy also provides three helper functions for common stacking directions:

hstack() · Stack Horizontally (Along Rows)

"h" stands for horizontal. hstack() joins arrays side by side · adding columns.

python
import numpy as np

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

arr = np.hstack((arr1, arr2))

print(arr)

Expected Output:

code
[1 2 3 4 5 6]

Explanation: Elements placed next to each other horizontally (in the same row). For 1D arrays, this is the same as concatenate().

vstack() · Stack Vertically (Along Columns)

"v" stands for vertical. vstack() stacks arrays on top of each other · adding rows.

python
import numpy as np

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

arr = np.vstack((arr1, arr2))

print(arr)

Expected Output:

code
[[1 2 3]
 [4 5 6]]

Explanation: arr2 is placed as a new row beneath arr1. The result has 2 rows and 3 columns.

dstack() · Stack Along Depth (Height)

"d" stands for depth. dstack() stacks arrays along a third axis · going deep, like layers.

python
import numpy as np

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

arr = np.dstack((arr1, arr2))

print(arr)

Expected Output:

code
[[[1 4]
  [2 5]
  [3 6]]]

Explanation:

  • This creates a 3D array with shape (1, 3, 2)
  • Each pair of matching-index elements is combined into the depth dimension
  • Position 0: [1, 4] (1 from arr1, 4 from arr2)
  • Position 1: [2, 5]
  • Position 2: [3, 6]

Quick summary table:

FunctionDirectionAdds New Axis?
hstack()Horizontal (side by side)No
vstack()Vertical (top and bottom)No
dstack()Depth (3rd dimension)Yes
stack()Any axisYes
concatenate()Any existing axisNo

✏️ Your Task
Practise what you just learned about Joining Arrays. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 9 of 10
Guided Practice Exercises
🎯 Your Challenge

Exercise 1 · Data Types

Objective: Practise checking and converting data types.

Scenario: You have a list of temperature readings stored as floats. You want to check the type, convert them to integers (for approximate comparison), and verify.

Steps:

  1. Create an array: temperatures = np.array([36.6, 37.1, 38.2, 36.9, 37.5])
  2. Print the dtype
  3. Convert to integers using .astype(int)
  4. Print the new array and its dtype
  5. Check: what happened to the decimal parts?

Expected output after step 4:

✏️ Task
Practise what you just learned about Guided Practice Exercises. Open your editor, type the examples above by hand, modify them, and observe what changes.
code
[36 37 38 36 37]
int64

Self-check questions:

  • Was 37.1 rounded to 37 or truncated to 37? (Both give 37 here, but what about 37.9?)
  • What would astype(bool) give for these temperature values?

Exercise 2 · Copy vs View

Objective: Understand when data is shared and when it is independent.

Scenario: You have a student grade array. You make a copy for a "backup" and a view for "display". You then update a grade.

Steps:

  1. Create: grades = np.array([85, 90, 78, 92, 88])
  2. Make a copy: backup = grades.copy()
  3. Make a view: display = grades.view()
  4. Update: grades[2] = 100
  5. Print grades, backup, and display
  6. Check .base for both backup and display

Expected output:

code
[85 90 100 92 88]   # grades (original, changed)
[85 90  78 92 88]   # backup (copy, unchanged)
[85 90 100 92 88]   # display (view, changed with original)

Self-check questions:

  • What does backup.base return?
  • What does display.base return?
  • Why is it important to use .copy() when you need an independent backup?

Exercise 3 · Shape and Reshape

Objective: Practise understanding and changing array structures.

Scenario: A data scientist has 24 daily measurements and needs to organise them into a weekly table.

Steps:

  1. Create: data = np.arange(1, 25) (creates an array from 1 to 24)
  2. Print the shape
  3. Reshape into 4 weeks × 6 days: data.reshape(4, 6)
  4. Print the reshaped array
  5. Reshape into 3D: 2 groups × 3 rows × 4 columns
  6. Flatten back to 1D using reshape(-1)

Hints:

  • np.arange(1, 25) creates [1, 2, 3, ..., 24]
  • 4 × 6 = 24 ✓
  • 2 × 3 × 4 = 24 ✓

Expected output for step 3:

code
[[ 1  2  3  4  5  6]
 [ 7  8  9 10 11 12]
 [13 14 15 16 17 18]
 [19 20 21 22 23 24]]

Exercise 4 · Iterating

Objective: Practise looping through multi-dimensional arrays.

Scenario: You have a 3×3 multiplication table and need to print every value.

Steps:

  1. Create: table = np.array([[1,2,3],[2,4,6],[3,6,9]])
  2. Use nested for loops to print every element
  3. Repeat using np.nditer()
  4. Use np.ndenumerate() to print each element with its position

Expected output for step 4:

code
(0, 0) 1
(0, 1) 2
(0, 2) 3
(1, 0) 2
(1, 1) 4
(1, 2) 6
(2, 0) 3
(2, 1) 6
(2, 2) 9

Exercise 5 · Joining Arrays

Objective: Practise combining arrays in different directions.

Scenario: You have two student score tables from two classes and want to combine them.

Steps:

  1. Create: class1 = np.array([[80, 85], [90, 88]])
  2. Create: class2 = np.array([[75, 79], [95, 91]])
  3. Use concatenate(axis=0) to stack classes vertically (add more students)
  4. Use concatenate(axis=1) to add more subjects (side by side)
  5. Use vstack() for step 3 and hstack() for step 4

Expected output for step 3:

code
[[80 85]
 [90 88]
 [75 79]
 [95 91]]

Expected output for step 4:

code
[[80 85 75 79]
 [90 88 95 91]]

Phase 10 of 10
Common Beginner Mistakes

Mistake 1 · Assuming astype() Rounds (It Does Not)

python
# WRONG assumption:
arr = np.array([2.9, 3.7])
result = arr.astype(int)
# Beginner expects: [3, 4]
# Reality: [2, 3]  <- truncated, not rounded!

Fix: Use np.round() first if you need rounding, then convert.

python
arr = np.array([2.9, 3.7])
result = np.round(arr).astype(int)
print(result)  # Output: [3 4]

Mistake 2 · Treating a View Like a Copy

python
arr = np.array([1, 2, 3, 4, 5])
x = arr.view()        # NOT a copy!
x[0] = 999            # This changes arr too!
print(arr)            # [999  2  3  4  5] — surprise!

Fix: Use .copy() when you need independence.

python
x = arr.copy()        # Now truly independent
x[0] = 999
print(arr)            # [1 2 3 4 5] — original is safe

Mistake 3 · Invalid Reshape (Forgetting to Check Total Elements)

python
arr = np.array([1, 2, 3, 4, 5, 6])
# WRONG: 6 elements cannot form a 2x4 grid (2x4=8, not 6)
result = arr.reshape(2, 4)  # ValueError!

Fix: Always verify that new_shape[0] × new_shape[1] × ... = original_total_elements.

python
# Correct options for 6 elements:
arr.reshape(2, 3)   # 2x3=6 ✓
arr.reshape(3, 2)   # 3x2=6 ✓
arr.reshape(6, 1)   # 6x1=6 ✓
arr.reshape(1, 6)   # 1x6=6 ✓
arr.reshape(2, 3, 1) # 2x3x1=6 ✓

Mistake 4 · Using -1 in More Than One Dimension

python
arr = np.array([1, 2, 3, 4, 5, 6])
# WRONG: can only use -1 once
result = arr.reshape(-1, -1)  # ValueError!

Fix: Only one dimension can be -1. Specify all other dimensions explicitly.

python
result = arr.reshape(2, -1)  # NumPy calculates: 6/2=3, so shape (2,3) ✓

Mistake 5 · Concatenating Arrays with Mismatched Shapes

python
arr1 = np.array([[1, 2, 3]])      # shape (1, 3)
arr2 = np.array([[4, 5], [6, 7]]) # shape (2, 2)
result = np.concatenate((arr1, arr2), axis=0)  # Error! Different column counts

Fix: All arrays being concatenated must match in ALL dimensions except the one being combined.

python
arr1 = np.array([[1, 2], [3, 4]])  # shape (2, 2)
arr2 = np.array([[5, 6], [7, 8]])  # shape (2, 2)
result = np.concatenate((arr1, arr2), axis=0)  # shape (4, 2) ✓

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes. Open your editor, type the examples above by hand, modify them, and observe what changes.
🏗️ Build It — Mini Project
Stage 1 · Setup and Data Inspection

Goal: Build a small data pipeline that uses all six topics from this lesson.

Scenario: You are a data analyst at a school. You have raw exam score data as a flat list of 12 scores (from 4 students across 3 subjects). You need to organise, inspect, and extend this data.

Stage 1 · Setup and Data Inspection

starter.py
import numpy as np

# 12 raw scores: 4 students, 3 subjects each
raw_scores = np.array([72, 85, 90, 68, 79, 88, 95, 77, 82, 55, 91, 63])

# Step 1: Check the dtype
print("Data type:", raw_scores.dtype)

# Step 2: Check the shape
print("Shape:", raw_scores.shape)

Lesson 25 complete! 🎉

You covered: