Skip to main content

Chapter 19 Using Random Numbers and Probability Models

To understand basic probability, often problems are examined that use combinatorics or counting techniques to solve them. Consider
“A round table has 7 chairs around it. Mary and her friend Alisha and 5 other people are given seats at the table in a random manner. What is the probability that Mary and Alisha sit next to each other?”
Although it is an good skill to solve problems like these using counting techniques, we examine some ways to use simulation to find the solution in this chapter.
Before getting started, we will set the seed so the random numbers that appear here will match those if you, the reader, run these same commands
using Random
Random.seed!(1234)

Section 19.1 Flipping Coins

Recall as we showed in Chapter 18, we can generate 100 coin flips by
coins = rand(Bool,100)
(Note: the result looks like an array of 0s and 1s, but look at the type, Vector{Bool}, which says it is a vector or 1-dimensional boolean array.)
We can determine the number of heads (when the result is 1), by sum(coins) which is 58.

Subsection 19.1.1 Flipping multiple coins

Another simple example is to flip multiple coins and generally count the number of heads or tails seen. Consider flipping 3-coins--perhaps a penny, nickel and dime--and counting the number of heads. We then do that 3-coin flip a larger number of times.
We can do this with
coins3 = rand(Bool,100,3)
and the top 10 lines of this is:
100×3 Matrix{Bool}:
  1  1  0
  0  0  1
  1  0  0
  0  1  1
  0  1  1
  0  1  0
  1  0  1
  0  0  1
  1  0  0
  0  0  0
Each row contains the coin flips. Each 1 represents a head and 0 is a tail. If we are interested in the sum of the number of heads, we can do this with the mapslices functions seen in Chapter 6.
num_heads = mapslices(sum,coins3;dims=[2])
The first few elements of the array is [2; 1; 1; 2; 2; 1; 1; 0 ...]. We would like a plot of this, however need to do a bit of work first. The hope is that for each number of heads 0, 1, 2 and 3, we create a bar plot with the number of heads for each or the fraction of heads. Although we have the tools to do this, since this is a common thing to do in statistics, there is a function in the StatsBase package.
using StatsBase
and then the counts function ( Read the online documentation) can be used:
head_count = counts(num_heads,0:3)
returns a vector of how many of number of heads fall into each number. The result is the vector [8 39 43 10].
and a nicer way to plot the histogram is
barplot(0:3,head_count/sum(head_count), strokewidth = 1)
where the strokecolor attribute puts a thin border around each bar. The result is
(for accessibility)
Figure 19.1.
which generates an approximate probability distribution for flipping 3 coins and s umming the number of heads. And to compare the simulation with the probability density function that we found in Chapter 18, we will plot a side-by-side comparison of the two with the following. Note that the function groupedbar is part of the StatsPlots package:
barplot(repeat(0:3,2),vcat(head_count/sum(head_count),[1/8, 3/8, 3/8, 1/8]),
dodge = repeat(0:3,inner = 2), color=repeat(1:2, inner = 4))
resulting in
(for accessibility)
Figure 19.2.
This is not the best looking plot and would like to include a legend. Here’s a little cleaned up version of this and then will explain all of the arguements and attributes.
values = 0:3
heights1 = head_count/sum(head_count)
heights2 = [1/8, 3/8, 3/8, 1/8]
colors = Makie.wong_colors()  # use more attractive colors
fig = Figure()
ax = Axis(fig[1,1], title = "Comparison of simulation and known PDF of Flipping 3 coins")
xvals = repeat(values,2)
heights = vcat(heights1, heights2)
barplot!(ax, xvals, heights, dodge = repeat(values,inner = 2),
  color=colors[repeat(1:2, inner = length(values))], strokewidth = 1
  )
labels = ["simulation", "PDF"]
elements = [PolyElement(polycolor = colors[i]) for i in 1:length(labels)]
axislegend(ax, elements, labels)
fig
and the resulting plot is
(for accessibility)
Figure 19.3.
This is reasonably complicated and for a grouped bar plot and there are many ways to do this using the barplot method and we go into a lot of details in Chapter 32. To use this with other data, you should be able to update the first 3 lines of this code.

Section 19.2 Rolling Dice

A relatively-simple example is that of rolling dice. As we saw in Chapter 18, if we have a fair, six-sided die, the the probability of any of the numbers coming up is 1/6. We can simulate that using random numbers in the following way:
S100 = rand(1:6,100)
generated 100 numbers taken from the set \(\{1,2,3,4,5,6\}\text{.}\) Running this command
 1 
Don’t forget that when running code with random numbers, you won’t get the exact same results, but the spirit should be the same.
results in a vector of length 100 with random numbers between 1 and 6.
The principal of the Law of Large Numbers is that as a experiment is repeated, as the number of times increases, the distribution of results tends toward the underlying distribution. If we determine if this is true, as the number of time we roll a die increases, the probability of any one number appearing gets closer to 1/6. For example, to test this, try
p = count(a->a==3,rand(1:6,1_000))
and then evaluating p/1_000 results in 0.190, which is reasonably close to 1/6.

Check Your Understanding 19.4.

(a)
Trying increasing the number of random numbers used in the above example. You should notice that as that number gets larger, the resulting probability gets closer to 1/6.
(b)
What does
p = count(a -> a % 2 == 0, rand(1:6,1000))
measure in the sense of a rolling a fair 6-sided die? Does the result make sense?

Section 19.3 Rolling 2 dice

How do we handle the rolling of two dice? We will do this in a similar manner to flipping 3 coins above. First, we’ll start with an array with each row having 2 dice. This would be a simulation of rolling 10,000 pairs of dice.
 dice2=rand(1:6,10_000,2) 
which creates a matrix of size of 10,000 by 2 of random numbers between 1 and 6. Each row represents a pair of dice and the sum of the dice is just the sum of each row which we can get from
 dsum = mapslices(sum,dice2;dims=[2]) 
which sums along the rows. This is 10,000 rolls of 2 dice with the sum recorded. First, to get an idea of the distribution of the dice sums, let’s plot the distribution of the values:
fig = Figure()
ax = Axis(fig[1,1], xticks=2:12)
barplot!(ax, 2:12, counts(dsum,2:12)/sum(dsum), strokewidth = 1)
fig
The plot generated is
(for accessibility)
Figure 19.5.

Check Your Understanding 19.6.

Section 19.4 Other Probability Models

Let’s return to the problem that problem at the beginning of this section. We will solve this problem using the random modeling in this chapter.
First let’s first store the names of the people at the take as an array
table_names = ["Alisha", "Mary", "p1", "p2", "p3", "p4", "p5"]
where we use generic names for the other 5 people.
 2 
Although variable name usually don’t matter what they are called, names cannot be used. There are many function and variable names that cannot be used. This is one example.
We will take random permutations of this array below and the determine if Alisha and Mary are next to each other.
Before we find a random permutation, let’s write a function that takes an array of names and returns true if they are next to each other and false if not.
function nextToEachOther(names::Vector{String})
  # return true or false
end
Here’s a way to think about this:
  1. Find the position in the array where Alisha is sitting.
  2. Find the position in the array where Mary is sitting.
  3. Determine if the two numbers are next to each other. Don’t forget that this could include positions 1 and 7.
The following is a relatively simple function is
function nextToEachOther(names::Vector{String})
  a = findfirst(name -> name=="Alisha",names)
  m = findfirst(name -> name=="Mary",names)
  abs(a-m) == 1 || abs(a-m) == length(names)-1
end
where the findfirst function returns the index of the array where the function is true (that is where the two friends are sitting). To test this:
nextToEachOther(["Alisha", "Mary", "p1", "p2", "p3", "p4", "p5"])
returns true.
nextToEachOther(["Alisha", "p1", "Mary", "p2", "p3", "p4", "p5"])
returns false.
nextToEachOther(["Alisha","p1", "p2", "p3", "p4", "p5", "Mary"])
returns true.

Subsection 19.4.1 Random Permutations

We now return to the problem to study the probability. The shuffle command in the Random package
 3 
The Random package is a built-in package and doesn’t need to be added, but just enter using Random
takes any array and shuffles (permutes) the contents in a random manner. For example:
shuffle(table_names)
returns ["p3" "Alisha" "Mary" "p5" "p2" "p4" "p1"], then we can test if they are sitting next to each other. The follow repeats this a large number of times:
function numTimes(trials::Integer)
  s = 0  # keeps track of how many times they sit next to each other
  for i=1:trials
    if nextToEachOther(shuffle(table_names))
      s += 1
    end
  end
  s/trials
end
and then the fraction of times is found with numTimes(10_000) resulting in 0.3303.
The true value of the probability can be found in the following way. Mary sits in one of the seven chairs. Alisha has a equal chance of sitting in one of the remaining 6 chairs. Two of the chairs are next two Mary, so the probability is 2/6 or 1/3. The result we see above is close to this value.

Section 19.5 Calculating \(\pi\) using pseudo random numbers

You probably know the first handful of digits of \(\pi\) and recall that probably the first place that you saw this was with circles. But how do we know what the digits of \(\pi\) actually are. There are a number of ways to find the digits of \(\pi\) and in fact, Chapter 48 presents some interesting ways of calculating it.
Here we will present two ways to calculate \(\pi\) using random numbers.

Subsection 19.5.1 Buffon’s Needle Experiment

In the 18th Century, Georges-Louis Leclerc, Comte de Buffon considered the following problem. On a floor (or table), draw lines that are parallel and \(t\) units apart. Toss needles of length \(\ell\) onto the floor and count the number that cross one of the lines. The following diagram is helpful:
Figure 19.7.
A probability analysis shows that the probability that any needle crosses a line is:
\begin{equation*} p = \frac{2}{\pi}\frac{\ell}{t} \end{equation*}
if \(\ell < t\text{.}\)
So theoretically, one could sit and toss needles onto a floor to determine the value of \(\pi\) by counting the number that cross the lines and those that don’t. This would be tedious and possibly prone to error as the number of needles rises.
More details on this experiment is given on the wikipedia page

Subsection 19.5.2 Circle in the Square

The Buffon Needle experiment is fascinating in many ways but there is a much easier way to generate results in a computer simulation. This is called the Circle in the Square approach.
Consider a square given by \(\{ (x,y)\; | \; 0\leq x\leq 1, 0 \leq y \leq 1\}\text{,}\) which is just the first unit in both \(x\) and \(y\) in the first quadrant. We generate a number of random points (technically they are uniformly distributed). Consider
pts=rand(100,2)
fig = Figure()
ax = Axis(fig[1,1], aspect = 1)
scatter!(ax, pts[:,1],pts[:,2])
fig
(for accessibility)
Figure 19.8.
If we draw a circular arc and then count the number of points within the arc:
fig = Figure()
ax = Axis(fig[1,1], aspect = 1)
scatter!(ax, pts[:,1],pts[:,2])
t = LinRange(0,pi/2,100)
lines!(ax,cos.(t),sin.(t), color = :red)
fig
where the circle has been plotted parametrically and in the first quadrant we use \(0 \leq t \leq \pi/2\text{.}\) The result is
(for accessibility)
Figure 19.9.
The fraction of points should be about the fraction of the area within the arc or \(\pi/4\text{.}\) (Note: the plot! function plots on top of the current plot instead of making a new one. It also performs a parametric plot--see Chapter 15--of the circle.)
We can calculate this value by first using the mapslices function on each row to calculate the distance
dist=mapslices(pt->sqrt(pt[1]^2+pt[2]^2),pts;dims=[2])
and then count the number of the points within the circle or the distance is less than 1.
numpts = count(d -> d < 1, dist)
and the results is 77. Since the fraction of the points is an estimate for \(\pi/4\text{,}\) then we can say that
\begin{equation*} \pi \approx 4 \cdot numpts \cdot total\_points \end{equation*}
so in this case the estimate is 3.08. Clearly this is a terrible estimate of \(\pi\text{,}\) but increasing the number of points should improve it.
Instead of just repeating the same code with a different number of points, we will create a function that estimates \(\pi\) based on the number of points originally.
function calcPi(total_points::Integer)
  pts=rand(total_points,2)
  dist=mapslices(pt -> pt[1]^2+pt[2]^2, pts; dims=[2])
  4*count(d -> d < 1,dist)/total_points
end
and we haven’t included the square root in the distance calculation because all we are trying to determine is which points are within the unit circle. This uses the square distance, so the result is the same.
Let’s run it with 10,000 points using the command:
 approx_pi = calcPi(10_000) 
and we get the result 3.1632
Check Your Understanding 19.10.
(a)
for \(N=10^5\text{,}\) \(N=10^6\) and \(N=10^7\) find the relative error of the estimate using your function and using the built-in value pi.
(b)
Recall that earlier in the book, we determine that creating an array is one of the slower parts of code. In this case, it is not needed. Rewrite the calcPi function to not generate a matrix of total_points rows.
Hint.
Write a for loop and inside the loop generate a single point and determine whether or not it is in the circle. Time this version and compare to the calcPi above.