import numpy as npModuleNotFoundError: No module named 'numpy'
Module 3: NumPy
Suppose you are on a game show, and you are given the choice of three doors: Behind one door is a car; behind the others, goats. You pick a door, say No. 1, and the host, who knows what is behind the doors, opens another door, say No. 3, which has a goat. He then says to you, “Do you want to pick door No. 2?” Is it to your advantage to switch your choice?
![]()
To answer this question, we can simulate the game a big number of times. In the first run of simulations we keep the door we chose, in the second run we switch doors. Then we compare our winning percentage.
For this lesson we will only need NumPy.
import numpy as npModuleNotFoundError: No module named 'numpy'
We can use numpy arrays to represent the doors on each game, in the form of a matrix \(\boldsymbol{x}\) with shape \((N, 3)\) where \(N\) is the number of games we play. Values \(x=0\) represent a goat, while \(x=1\) represents the car.
# Let us start with a low number of games
n_games = 10
# Initialize the matrix, filled with zeros (only goats)
x = np.zeros((n_games, 3))
print(x)NameError: name 'np' is not defined
What is the shape of x?
print("Shape of the x:", x.shape)NameError: name 'x' is not defined
What is the type of variable x?
print("Type of x:", type(x))NameError: name 'x' is not defined
What about the type of its elements?
print("Types inside the x:", x.dtype)NameError: name 'x' is not defined
By default, NumPy initializes arrays of zeros with a floating-point data type. For this game we only need 0 and 1, so we can request integers directly.
x = np.zeros((n_games, 3), dtype=int)
print("Types inside x:", x.dtype)
print(x)NameError: name 'np' is not defined
Lets review some basic concepts:
Knowing how to do this will help in the next steps.
print("First row of x (doors of the first game): ", x[0, :])NameError: name 'x' is not defined
print("First column of x (first door of each game):", x[:, 0])NameError: name 'x' is not defined
print("First element of x (first door of first game): ", x[0, 0])NameError: name 'x' is not defined
As of now, all doors have a goat behind (a 0).
NumPy arrays are mutable, so we can assign new values to their elements.
# Add a car to the first door of the first game
x[0, 0] = 1
print(x)NameError: name 'x' is not defined
# Add a car to the first door of every game
x[:, 0] = 1
# This is the same as saying
# For each row `:` in `x`
# Change the first value (index `0`) to `1`
print(x)NameError: name 'x' is not defined
Now there is a car for each game, but all cars are always at the first door.
![]()
If we want the car to be placed randomly, we must use advanced indexing.
Numpy arrays can be indexed using lists, as long as all lists have the same length.
list_0 = [a0, b0, c0]
list_1 = [a1, b1, c1]
# Being x a 2D array
x[list_0, list_1]
# Is the same as indexing the following elements
x[a0, a1], x[b0, b1], x[c0, c1]# Sample matrix
y = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
# Take elements (0, 0), (1, 1) and (2, 2)
idx = [0, 1, 2]
idy = [0, 1, 2]
print(y[idx, idy])NameError: name 'np' is not defined
If the index arrays do not have compatible shapes, indexing fails.
# Sample matrix
y = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
# Take elements (0, 0), (1, 1) and (2, 2)
idx = [0, 1]
idy = [0, 1, 2]
print(y[idx, idy])Pay attention to the structure of advanced indexing:
# Good
x[[a0, b0, c0], [a1, b1, c1]]
# Bad
x[[a0, a1], [b0, b1], [c0, c1]]# Sample matrix
y = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
# Wrong indexing
print(y[(0, 0), (1, 1), (2, 2)])# Sample matrix
y = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
# Take elements (0, 0), (1, 1) and (2, 2)
idx = [0, 1, 2]
idy = [0, 1, 2]
# List cannot be negative
print(y[-idx, -idy])# Sample matrix
y = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
# Take elements (0, 0), (1, 1) and (2, 2)
idx = [-1, -2, -3]
idy = [0, 1, 2]
# List cannot be negative, but indices inside can!
print(y[idx, idy])NameError: name 'np' is not defined
The advanced indexing will allow us to choose one index per row, and fill it with “1” (a car).
We just need to generate these indices randomly. For this we can use np.random module.
# Generate a random integer in the range [0, 2)
n = np.random.randint(0, 3)
print(n)# You could import the subpackage, but it is not necessary
import numpy.random as rn
print(rn.randint(0, 10))ModuleNotFoundError: No module named 'numpy'
# Generate 10 random integers in the range [0, 2)
n = np.random.randint(0, 3, size=10)
print(n)# Setting a random seed ensures reproducibility of results
np.random.seed(14) # You can use any integer here
# Generate an array of 10 random integers between 0 and 99
n = np.random.randint(0, 100, size=10)
# With the seed set, this will produce the same array every time you run it
print(n)NameError: name 'np' is not defined
# Different seeds produce different outcomes
np.random.seed(20)
n = np.random.randint(0, 100, size=10)
print(n)NameError: name 'np' is not defined
Knowing this information, lets create a function that generates any number \(N\) of Monty Hall games, represented by a matrix \(N x 3\):
def monty_hall_init(n_games: int, verbose: bool = False) -> np.ndarray:
"""Create one randomly positioned car for each simulated game."""
games = np.zeros((n_games, 3), dtype=int)
game_indices = np.arange(n_games)
car_indices = np.random.randint(0, 3, size=n_games)
if verbose:
print("Game indices:", game_indices)
print("Car indices: ", car_indices)
games[game_indices, car_indices] = 1
return games
x = monty_hall_init(1000, verbose=True)
print(x)NameError: name 'np' is not defined
Review time! To make sure the array we generated is right, we want to check:
print("Range of values: ", x.min(), "to", x.max())NameError: name 'x' is not defined
print("Total number of cars:", x.sum())NameError: name 'x' is not defined
print("Average of cars in total:", x.mean())NameError: name 'x' is not defined
print("Average of cars per door: ", x.mean(axis=0))NameError: name 'x' is not defined
print("Number of cars per attempt:", x.sum(axis=1))NameError: name 'x' is not defined
Time to play the Monty Hall game!
In this first run, we (the guest) will pick one door and stick to it. There is no need to simulate what the host reveals.
# Reinitialize the matrix of doors, this time with 10,000 games
game = monty_hall_init(10000)
print(game.mean(axis=0))NameError: name 'monty_hall_init' is not defined
# Reinitialize the matrix of doors, this time with 10,000 games
game = monty_hall_init(10000)
# Get the number of games
n_games, n_doors = game.shape
# Initialize the number of times we find the car, as 0
sum_car = 0
# Loop through every game
for id_game in range(n_games):
# Make a random choice between the doors (0, 1, 2)
id_guest = np.random.randint(0, 3)
# If we found the car, `sum_car` will increase by 1
sum_car += game[id_game, id_guest]
print("Win probability:", sum_car / n_games)NameError: name 'monty_hall_init' is not defined
![]()
To handle the host intervention, we will use sets:
game = [0, 0, 1]
my_pick = input("Pick a door: ") # Im picking door 1 (index 0)
my_pick = int(my_pick)
# How can presenter pick door 3 (index 2)?
doors = {0, 1, 2}
# Find the index of the value "1" in a list
idx_car = game.index(1)
doors = doors - {idx_car, my_pick}
print(doors)game = [0, 0, 0]
idx_car = np.random.randint(0, 3)
game[idx_car] = 1
print(game)
my_pick = input("Pick a door: ") # Im picking door 1 (index 0)
my_pick = int(my_pick)
# How can presenter pick door 3 (index 2)?
doors = {0, 1, 2}
# Find the index of the value "1" in a list
idx_car = game.index(1)
doors = doors - {idx_car, my_pick}
print(doors)# Lets start with just one game
game = monty_hall_init(1)
print(game)NameError: name 'monty_hall_init' is not defined
We guess one door at random. How can we make the host reveal one of the other two doors that have a goat?
# Make a random choice between the doors (0, 1, 2)
id_guest = np.random.randint(0, 3)
print(f"Our choice: {id_guest}")NameError: name 'np' is not defined
np.argmax(x) returns the indices of the maximum values along an axis.
# Where is the car? Check our array with car indexes
id_car = np.argmax(game[0])
print(f"The car is here: {id_car}")NameError: name 'np' is not defined
The host must reveal a door that is neither what we picked, nor the door with the car. This is a subset operation!
# The host can open the door that:
# - Has not been choseen by the guest
# - Does not have the car
ls_id_host = list(set([0, 1, 2]) - set([id_guest, id_car]))
print(f"The doors that the host can show are: {ls_id_host}")NameError: name 'id_guest' is not defined
Actually, this set operation can be performed in NumPy using setdiff1d:
# The host can open the door that:
# - Has not been choseen by the guest
# - Does not have the car
ls_id_host = np.setdiff1d([0, 1, 2], [id_guest, id_car])
print(f"The doors that the host can show are: {ls_id_host}")NameError: name 'np' is not defined
In order to choose between the list of options, we can use np.random.choice.
id_host = np.random.choice(ls_id_host)
print(f"The host reveals door: {id_host}")NameError: name 'np' is not defined
Finally, we have to choose the door that was picked neither by us nor by the host.
# After the host reveals the door, we (the guest) change our choice
ls_id_swap = np.setdiff1d([0, 1, 2], [id_guest, id_host])
print(f"Our options to swap are: {ls_id_swap}")NameError: name 'np' is not defined
id_swap = ls_id_swap[0]
print(f"We swap to: {id_swap}")NameError: name 'ls_id_swap' is not defined
# Did we won?
is_car = game[0, id_swap]
print(is_car)NameError: name 'game' is not defined
Now we can implement this logic inside a for loop:
# Start the game
game = monty_hall_init(10000)
# Get the number of games
n_games, n_doors = game.shape
# Initialize the number of times we find the car, as 0
sum_car = 0
# Loop through every game
for id_game in range(n_games):
# Make a random choice between the doors (0, 1, 2)
id_guest = np.random.randint(0, 3)
# Where is the car? Check our array with car indexes
id_car = np.argmax(game[id_game])
# The host can open the door that:
# - Has not been choseen by the guest
# - Does not have the car
ls_id_host = np.setdiff1d([0, 1, 2], [id_guest, id_car])
id_host = np.random.choice(ls_id_host)
# After the host reveals the door, we (the guest) change our choice
ls_id_swap = np.setdiff1d([0, 1, 2], [id_guest, id_host])
id_swap = ls_id_swap[0]
# If we found the car, `sum_car` will increase by 1
sum_car += game[id_game, id_swap]
print("Win probability:", sum_car / n_games)NameError: name 'monty_hall_init' is not defined
ls = [2]
print(ls[0])2
Alternative: To find which door should the host open, instead of set difference, we can apply the following logic.
(A) Assuming the guest chooses wrong the first time, we have two doors that the host cannot open, id_guest and id_host. We can see that the following operation will always get us that door:
id_host = (3 - id_guest - id_car)
Cases:
(3 - 0 - 1) = 2 → 2 % 3 = 2(3 - 0 - 2) = 1 → 1 % 3 = 1(3 - 1 - 2) = 0 → 0 % 3 = 0Why add % 3? Is our way to ensure the ID stays in the interval (0, 2). In the cases above it is not required but will be necessary for the following scenario.
(B) What if the guest first choice was right?
(3 - 0 - 0) = 3 → 3 % 3 = 0(3 - 1 - 1) = 1 → 1 % 3 = 1(3 - 2 - 2) = -1 → -1 % 3 = 2Then we can adjust by randomly adding 1 or 2 so that the host picks one of the other two doors.
if id_host == id_car:
id_host = id_host + np.random.randint(1, 3)
id_host = id_host % 3
# Start the game
game = monty_hall_init(10000)
# Get the number of games
n_games, n_doors = game.shape
# Initialize the number of times we find the car, as 0
sum_car = 0
# Loop through every game
for id_game in range(n_games):
# Make a random choice between the doors (0, 1, 2)
id_guest = np.random.randint(0, 3)
# Where is the car? Check our array with car indexes
id_car = np.argmax(game[id_game])
# Choose the ID for the host
id_host = (3 - id_guest - id_car) % 3
if id_host == id_car:
id_host += np.random.randint(1, 3)
id_host %= 3
# Apply the same logic to choose the door we swap to
id_swap = (3 - id_guest - id_host) % 3
# If we found the car, `sum_car` will increase by 1
sum_car += game[id_game, id_swap]
print("Win probability:", sum_car / n_games)NameError: name 'monty_hall_init' is not defined
The code we have above works fine, but it takes some time to compute one million simulations. We can speed up this process by being clever with NumPy.
By the end of module 3, we will see how to write faster NumPy code! Our key goal: remove for loops and replace it with NumPy broadcasting.
randint(0, 3) can produce 0, 1, or 2.Explain why ten simulated games are not enough to estimate the switching probability reliably. What changes as the number of simulations increases?
Work through the Session 16 homework exercises available here.