6.7. 📥 Demonstration: Coin tossing (with widget)#
In this notebook we consider the problem of attempting to determine the probability that a particular coin will come up heads on any given toss, \(p_h\), based on data as to how many heads (and tails) it produces in \(N\) tosses. This example is gratefully adapted from the example in Sivia’s book.
The data \(D\) will then be the number of heads obtained in \(N\) trials. The probability of obtaining a particular number of heads will be a function of \(p_h\). This is the likelihood piece of Bayes’ theorem. Note that the outcome is discrete (either heads or tails), and the number of heads obtained in \(N\) trials is an integer, but \(p_h\) can be any real number \(0 \leq p_h \leq 1\), and all our output pdfs are continuous functions of \(p_h\) in the interval \(0 < p_h < 1\).
Meanwhile we can represent different prior knowledge and/or beliefs about \(p_h\) in the prior, i.e., \({\rm p}(p_h|I)\). \(I\) could be information regarding the character of the coin flipper, it could be based on a previous experiment (we managed to get hold of the coin and flip it a few times before hand!), or it could be an “ignorance prior”, the formulation of which we will come back to later in the book.
Python/Jupyter set up#
import numpy as np
import scipy.stats as stats
from scipy.stats import norm, uniform
import matplotlib.pyplot as plt
import ipywidgets as widgets
from ipywidgets import HBox, VBox, Layout, Tab, Label, Checkbox, Button
from ipywidgets import FloatSlider, IntSlider, Play, Dropdown, HTMLMath
from IPython.display import display
import seaborn as sns
sns.set()
sns.set_context("talk")
Bayesian updating#
One of the key points of this exercise, is that with each flip of the coin we acquire more information on the value of \(p_h\). The logical thing to do is to update the state of our belief, our pdf for \(\mathrm{p}(p_h|\mbox{\# tosses, \# heads},I)\) each time the number of coin tosses is incremented by 1. The pdf will tend to get narrower, i.e., our state of knowledge of \(p_h\) more definite, as we acquire more data.
Note that in what follows we exploit the fungibility of mathematical symbols to let \(I\) stand for different things at different stages of the coin tossing experiment. If we are going to “update” after every coin toss then \(D\) is just the result of the \(N\)th oin toss and \(I\) is what we know about the coin after \(N-1\) coin tosses.
Main code for coin-flipping UI#
User-interface for coin-flipping#
Take a look at the information under the Help tab to find out about what the controls do, what the priors are, etc.
display(UI_box)
Widget user interface features:
tabs to control parameters or look at documentation
set the true \(p_h\) by the slider
press “Next” to flip “jump” # of times
plot shows updating from three different initial prior pdfs
Degree of belief intervals#
Now we are going to compute some Bayesian confidence intervals, aka DoB intervals, aka credibility intervals… You should go through this section once using the numbers provided, and then come back and run it again (or copy-paste the relevant lines) for other results from the widget. You can also employ priors other than those provided if you wish, but that takes a bit more work.
First we paste code from the “Playing with pdfs” notebook:
def dist_stuff(dist):
"""
Finds the median, mean, and 68%/95% credible intervals for the given
1-d distribution (which is an object from scipy.stats).
"""
# For x = median, mean: return x and the value of the pdf at x as a list
median = [dist.median(), dist.pdf(dist.median())]
mean = [dist.mean(), dist.pdf(dist.mean())]
# The left and right limits of the credibility interval are returned
cred68 = dist.interval(0.68)
cred95 = dist.interval(0.95)
return median, mean, cred68, cred95
def dist_mode(dist, x):
"""
Return the mode (maximum) of the 1-d distribution for array x.
"""
x_max_index = dist.pdf(x).argmax()
# Return x of the maximum and the value of the pdf at that x
mode = [x[x_max_index], dist.pdf(x[x_max_index])]
return mode
Then we use this to write a function that will give us back the mean, 68%, and 95% intervals for a uniform prior.
def print_uniform_prior_measures(N,heads):
"""
Prints out the mean, and 68 and 95 CIs for a uniform prior.
Note that this means alpha=beta=1.
"""
median, mean, cred68, cred95 = dist_stuff(stats.beta(1+heads,1+N-heads))
mode = dist_mode(stats.beta(1+heads,1+N-heads),x)
print('For a uniform prior, and', heads, 'heads out of', N, 'tosses:')
print (f'Mean = {mean[0]:.3f}; Mode = {mode[0]:.3f}')
print (f'68% DoB interval = ({cred68[0]:.3f}, {cred68[1]:.3f})')
print (f'95% DoB interval = ({cred95[0]:.3f}, {cred95[1]:.3f})')
return
Now we fill in the values for N and heads from running the widget. Suppose it gave 3 heads out of 14 tosses.
print_uniform_prior_measures(14, 3)
For a uniform prior, and 3 heads out of 14 tosses:
Mean = 0.250; Mode = 0.213
68% DoB interval = (0.144, 0.356)
95% DoB interval = (0.078, 0.481)
print(f'The actual value of p_H is', prob_heads)
The actual value of p_H is 0.4
So, at least for the data this time, the 68% DoB for a uniform prior does not contain the true value, but the 95% one does.
def print_frequentist_estimators(N, heads):
"""
Finds the mean, and 68 and 95 CIs for a uniform prior.
Note that this means alpha_1=beta_1=1.
"""
mean = heads / N
sigma = np.sqrt(mean * (1 - mean) /N)
print(f'For {heads} heads out of {N} tosses,',
f'the frequentist 1-sigma interval =',
f'({mean-sigma:.3f} , {mean+sigma:.3f})')
return
print_frequentist_estimators(14, 3)
For 3 heads out of 14 tosses, the frequentist 1-sigma interval = (0.105 , 0.324)
Which Bayesian estimator is the frequentist mean closest to?
Is the frequentist 1\(\sigma\) interval the same as the Bayesian 68% DoB interval? If so, should they be? If not, why are they different?
Now we will also generate the summary statistics for the other priors. (What is coded is for the default values. After running through the exercise you can come back and try and change it; indeed, you should do that if you comparing to results where you altered the prior above.)
def print_likely_fair_prior_measures(N,heads):
"""
Prints out the mean, and 68 and 95 CIs for the "coin is likely fair"
prior. This means alpha = beta = 30.
"""
median, mean, cred68, cred95 = dist_stuff(stats.beta(30+heads,30+N-heads))
mode=dist_mode(stats.beta(1+heads,1+N-heads),x)
print(f'For the trusting-person\'s prior and {heads} heads',
f'out of {N} tosses:')
print (f'Mean = {mean[0]:.3f}; Mode = {mode[0]:.3f}')
print (f'68% DoB interval = ({cred68[0]:.3f}, {cred68[1]:.3f})')
print (f'95% DoB interval = ({cred95[0]:.3f}, {cred95[1]:.3f})')
return
print_likely_fair_prior_measures(14, 3)
For the trusting-person's prior and 3 heads out of 14 tosses:
Mean = 0.446; Mode = 0.213
68% DoB interval = (0.388, 0.503)
95% DoB interval = (0.335, 0.559)
def print_likely_unfair_prior_measures(N,heads):
"""
Prints out the mean, and 68 and 95 CIs for the "coin is likely unfair" prior. This means alpha=beta=0.2.
"""
median, mean, cred68, cred95 = dist_stuff(stats.beta(0.2+heads,0.2+N-heads))
mode=dist_mode(stats.beta(1+heads,1+N-heads),x)
print(f'For the nasty-suspicious-mind prior and {heads} heads '
f'out of {N} tosses:')
print (f'Mean = {mean[0]:.3f}; Mode = {mode[0]:.3f}')
print (f'68% DoB interval = ({cred68[0]:.3f}, {cred68[1]:.3f})')
print (f'95% DoB interval = ({cred95[0]:.3f}, {cred95[1]:.3f})')
return
print_likely_unfair_prior_measures(14, 3)
For the nasty-suspicious-mind prior and 3 heads out of 14 tosses:
Mean = 0.222; Mode = 0.213
68% DoB interval = (0.116, 0.329)
95% DoB interval = (0.056, 0.461)
So what is the best approach in this case? Objectivity? Trust? Suspicion?
Now having printed out the results for my particular coin-tossing experiment you should play with things and see what the different summary statistics give for other “experimental runs”.