D
Daniel Martin
So I can't sleep this morning, and I've got a big interview with
$PRESTIGOUS_COMPANY that I need to get my mind off of, so I thought
I'd share a little thing I did in Ruby a few months ago with the list.
If you've never heard of the "Monty Hall" problem, suffice it to say
that it's a big famous problem in probability
(http://en.wikipedia.org/wiki/Monty_Hall_problem) that is sometimes
used to see if people really, really know their probability. Anyway,
I've written a ruby class that allows one to investigate probabilities
such as the monty hall problem. For example:
puts('===Monty Hall, classic version===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
guessdoor = u.choose(1,2,3)
remaining_doors = [1,2,3].select{ |x|
x != treasuredoor and x != guessdoor }
showdoor = u.choose(*remaining_doors)
if (treasuredoor == guessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
Produces:
===Monty Hall, classic version===
You should switch
==> 2/3
You should stay
==> 1/3
Which is in fact the known probabilities. While playing with this, I
also have discovered subtleties to the problem, such as this
variation:
# variation where the host doesn't know where the treasure is either,
# and sometimes accidentally reveals the treasure. In that case,
# the producers declare a do-over, and the segment is retaped.
puts('===Monty Hall with ignorant host and do-overs===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
guessdoor = u.choose(1,2,3)
remaining_doors = [1,2,3].select{|x| x != guessdoor}
showdoor = u.choose(*remaining_doors)
u.assert(treasuredoor != showdoor) # i.e. we're not a do-over
if (treasuredoor == guessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
Produces:
===Monty Hall with ignorant host and do-overs===
You should switch
==> 1/2
You should stay
==> 1/2
This is in fact also known to be the correct result.
I can tackle other probability puzzles/games, such as:
# A family has two children. One of them is a boy.
# What is a chance that the other is a boy?
puts "===Boy-Girl problem==="
ProbabilityTree.runreport(1.to_r) { |u|
child1 = u.choose
boy, :girl)
child2 = u.choose
boy, :girl)
u.assert(child1 == :boy || child2 == :boy)
if child1 == :boy and child2 == :boy then
u.report "Both children are boys"
else
u.report "Other child is a girl"
end
}.display
===Boy-Girl problem===
Other child is a girl
==> 2/3
Both children are boys
==> 1/3
Anyway, I've found this fun to play around with, and useful in
clarifying many subtleties in probability problems and how to work
them. It takes a bit of practice to translate properly from problem
statement to code, and along the way gets you to think about exactly
what's going on.
For example, the boy-girl problem could be incorrectly coded as:
ProbabilityTree.runreport(1.to_r) { |u|
children = [u.choose
boy, :girl), u.choose
boy, :girl)]
choose_index = u.choose(0,1)
u.assert(children[choose_index] == :boy)
u.report "Other child is a #{children[1-choose_index]}"
}.display
Which is a different problem. (This problem is: "A couple has two
children, and you pick one at random. That child is a boy. What is
the chance that the other child is also a boy?")
You can also choose things with unequal probability, so you can
simulate loaded dice if you want to. I tried to simulate the
probabilities of blood types for my offspring given everything I know
about blood type distributions in the US and among my relatives, but
that never got anywhere in ruby; it's just a bit too slow. I do have
this same idea implemented in Haskell, however, and that was able to
produce an answer to the blood-type question in a reasonable amount of
time.
Anyway, here's the code:
____________probworlds.rb_________________
# whatever
class Amb
class ExhaustedError < RuntimeError; end
def initialize
@fail = [proc { fail ExhaustedError, "amb tree exhausted" }]
end
def choose(*choices)
enum_choose(choices)
end
def enum_choose(choices)
choices.each { |choice|
callcc { |fk|
@fail << fk
if choice.respond_to? :call
return choice.call
else
return choice
end
}
}
@fail.pop.call
end
def failure
choose
end
def assert(cond)
failure unless cond
end
end
class ProbabilityTree
private_class_method :new
private
def initialize(amb,one=nil)
@amb = amb
@reporthash = Hash.new(0)
@currentreport = nil
@currentprob = (one || 1)
@one = one || 1.0
@probtotal = 0
end
def save
[ @currentreport, @currentprob ]
end
def restore(savedstate)
@currentreport, @currentprob = savedstate
end
def normalize
@reporthash.each_key { |k| @reporthash[k] /= @probtotal }
@probtotal = 1 unless @reporthash.empty?
@reporthash
end
def succeed
@reporthash[@currentreport] += @currentprob
@probtotal += @currentprob
@amb.failure
end
public
def failure
@amb.failure
end
def choose_h(h)
choose_p(*(h.to_a))
end
def choose(*rest)
choose_p(*(rest.map {|x| [x, @one/rest.size]}))
end
def choose_p(*args)
savedstate = save()
x, prob = @amb.enum_choose(args)
restore(savedstate)
@currentprob *= prob
return x
end
def report(string)
if (@currentreport and @currentreport != '')
@currentreport = @currentreport.to_s
@currentreport += "\n#{string.to_s}"
else
@currentreport = string
end
end
def assert(cond)
failure unless cond
end
def ProbabilityTree.universe(one=nil)
ae = Amb.new
ptree = new(ae,one)
if (ae.choose(true,false))
yield ptree
ptree.send
succeed)
else
return ptree.send
normalize)
end
end
def ProbabilityTree.report(h)
rval = ""
h.keys.sort {|x,y| h[y] <=> h[x]}.each { |k|
rval += "#{k}\n==>\t#{h[k]}\n"
}
rval
end
def ProbabilityTree.runreport(one=nil)
report(universe(one) {|u| yield u})
end
end
# reference http://en.wikipedia.org/wiki/Three_cards_problem
puts 'Three-card problem:'
ProbabilityTree.runreport { |u|
card = u.choose('bw', 'ww', 'bb')
side = u.choose(0,1)
u.assert(card[side] == ?b);
u.report "Other side is #{card[1-side,1]}"
}.display
puts 'Three-card problem with someone who always shows you the black
side if possible:'
ProbabilityTree.runreport { |u|
card = u.choose('bw', 'ww', 'bb')
side = u.choose(0,1)
if card[side] != ?b and card[1-side] == ?b then
side = 1-side
end
u.assert(card[side] == ?b);
u.report "Other side is #{card[1-side,1]}"
}.display
# Monty Hall
require "rational"
# standard version
puts('===Monty Hall, classic version===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
guessdoor = u.choose(1,2,3)
remaining_doors = [1,2,3].select{|x|
x != treasuredoor and x != guessdoor }
showdoor = u.choose(*remaining_doors)
if (treasuredoor == guessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
# variation where the host doesn't know where the treasure is either,
# and sometimes accidentally reveals the treasure. In that case,
# the producers declare a do-over, and the segment is retaped.
puts('===Monty Hall with ignorant host and do-overs===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
guessdoor = u.choose(1,2,3)
remaining_doors = [1,2,3].select{|x| x != guessdoor}
showdoor = u.choose(*remaining_doors)
u.assert(treasuredoor != showdoor) # i.e. we're not a do-over
if (treasuredoor == guessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
# The lesson?
# use assert for stuff that "just happened". Pass a
# different list to choose for stuff that was deliberate.
# You and Bob both choose doors. Monty Hall tells one of you
# to go home, and opens the losers door, because they chose poorly.
# Should the remaining person switch doors or stick with their
# original guess?
puts('===Monty Hall with Bob===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
myguessdoor = u.choose(1,2,3)
bobguessdoor = u.choose(*([1,2,3].select{|i| i != myguessdoor}))
elimchoices = [[:me,myguessdoor],[:bob,bobguessdoor]]
elimchoices = elimchoices.select{|i| i[1] != treasuredoor}
elimchoice = u.choose(*elimchoices)
u.assert(elimchoice[0] == :bob)
if (treasuredoor == myguessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
# Same as above, but the host hates Bob and will eliminate
# him early whenever possible. Now, if Bob is eliminated
# what should you do?
puts('===Monty Hall hates Bob===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
myguessdoor = u.choose(1,2,3)
bobguessdoor = u.choose(*([1,2,3].select{|i| i != myguessdoor}))
elimchoices = [[:me,myguessdoor],[:bob,bobguessdoor]]
elimchoices = elimchoices.select{|i| i[1] != treasuredoor}
elimchoice = nil
if elimchoices.size == 2 then
# If we're both still in the running to be eliminated, choose Bob
elimchoice = elimchoices[1]
else
# Choose whoever is left
elimchoice = elimchoices[0]
end
u.assert(elimchoice[0] == :bob)
if (treasuredoor == myguessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
# This doesn't work
puts "Sum - naive"
ProbabilityTree.runreport(1.to_r) { |u|
sum = 0
3.times {
i = u.choose(0,1)
sum += i
}
u.report sum
}.display
# This does work
puts "Sum - store state"
ProbabilityTree.runreport(1.to_r) { |u|
sum = 0
3.times {
psum = sum
i = u.choose(0,1)
sum = psum + i
}
u.report sum
}.display
# This works too
puts "Sum - hidden store state"
ProbabilityTree.runreport(1.to_r) { |u|
sum = 0
3.times {
sum += u.choose(1,0)
}
u.report sum
}.display
# and this
puts "Sum - choose state"
ProbabilityTree.runreport(1.to_r) { |u|
sum = 0
3.times {
i,sum = u.choose([0,sum],[1,sum])
sum += i
}
u.report sum
}.display
# and this
puts "Sum - inject"
ProbabilityTree.runreport(1.to_r) { |u|
sum = (0 ... 3).inject(0) { |s,|
s + u.choose(0,1)
}
u.report sum
}.display
# A family has two children. One of them is a boy.
# What is a chance that the other is a boy?
puts "===Boy-Girl problem==="
ProbabilityTree.runreport(1.to_r) { |u|
child1 = u.choose
boy, :girl)
child2 = u.choose
boy, :girl)
u.assert(child1 == :boy || child2 == :boy)
if child1 == :boy and child2 == :boy then
u.report "Both children are boys"
else
u.report "Other child is a girl"
end
}.display
$PRESTIGOUS_COMPANY that I need to get my mind off of, so I thought
I'd share a little thing I did in Ruby a few months ago with the list.
If you've never heard of the "Monty Hall" problem, suffice it to say
that it's a big famous problem in probability
(http://en.wikipedia.org/wiki/Monty_Hall_problem) that is sometimes
used to see if people really, really know their probability. Anyway,
I've written a ruby class that allows one to investigate probabilities
such as the monty hall problem. For example:
puts('===Monty Hall, classic version===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
guessdoor = u.choose(1,2,3)
remaining_doors = [1,2,3].select{ |x|
x != treasuredoor and x != guessdoor }
showdoor = u.choose(*remaining_doors)
if (treasuredoor == guessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
Produces:
===Monty Hall, classic version===
You should switch
==> 2/3
You should stay
==> 1/3
Which is in fact the known probabilities. While playing with this, I
also have discovered subtleties to the problem, such as this
variation:
# variation where the host doesn't know where the treasure is either,
# and sometimes accidentally reveals the treasure. In that case,
# the producers declare a do-over, and the segment is retaped.
puts('===Monty Hall with ignorant host and do-overs===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
guessdoor = u.choose(1,2,3)
remaining_doors = [1,2,3].select{|x| x != guessdoor}
showdoor = u.choose(*remaining_doors)
u.assert(treasuredoor != showdoor) # i.e. we're not a do-over
if (treasuredoor == guessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
Produces:
===Monty Hall with ignorant host and do-overs===
You should switch
==> 1/2
You should stay
==> 1/2
This is in fact also known to be the correct result.
I can tackle other probability puzzles/games, such as:
# A family has two children. One of them is a boy.
# What is a chance that the other is a boy?
puts "===Boy-Girl problem==="
ProbabilityTree.runreport(1.to_r) { |u|
child1 = u.choose
child2 = u.choose
u.assert(child1 == :boy || child2 == :boy)
if child1 == :boy and child2 == :boy then
u.report "Both children are boys"
else
u.report "Other child is a girl"
end
}.display
===Boy-Girl problem===
Other child is a girl
==> 2/3
Both children are boys
==> 1/3
Anyway, I've found this fun to play around with, and useful in
clarifying many subtleties in probability problems and how to work
them. It takes a bit of practice to translate properly from problem
statement to code, and along the way gets you to think about exactly
what's going on.
For example, the boy-girl problem could be incorrectly coded as:
ProbabilityTree.runreport(1.to_r) { |u|
children = [u.choose
choose_index = u.choose(0,1)
u.assert(children[choose_index] == :boy)
u.report "Other child is a #{children[1-choose_index]}"
}.display
Which is a different problem. (This problem is: "A couple has two
children, and you pick one at random. That child is a boy. What is
the chance that the other child is also a boy?")
You can also choose things with unequal probability, so you can
simulate loaded dice if you want to. I tried to simulate the
probabilities of blood types for my offspring given everything I know
about blood type distributions in the US and among my relatives, but
that never got anywhere in ruby; it's just a bit too slow. I do have
this same idea implemented in Haskell, however, and that was able to
produce an answer to the blood-type question in a reasonable amount of
time.
Anyway, here's the code:
____________probworlds.rb_________________
# whatever
class Amb
class ExhaustedError < RuntimeError; end
def initialize
@fail = [proc { fail ExhaustedError, "amb tree exhausted" }]
end
def choose(*choices)
enum_choose(choices)
end
def enum_choose(choices)
choices.each { |choice|
callcc { |fk|
@fail << fk
if choice.respond_to? :call
return choice.call
else
return choice
end
}
}
@fail.pop.call
end
def failure
choose
end
def assert(cond)
failure unless cond
end
end
class ProbabilityTree
private_class_method :new
private
def initialize(amb,one=nil)
@amb = amb
@reporthash = Hash.new(0)
@currentreport = nil
@currentprob = (one || 1)
@one = one || 1.0
@probtotal = 0
end
def save
[ @currentreport, @currentprob ]
end
def restore(savedstate)
@currentreport, @currentprob = savedstate
end
def normalize
@reporthash.each_key { |k| @reporthash[k] /= @probtotal }
@probtotal = 1 unless @reporthash.empty?
@reporthash
end
def succeed
@reporthash[@currentreport] += @currentprob
@probtotal += @currentprob
@amb.failure
end
public
def failure
@amb.failure
end
def choose_h(h)
choose_p(*(h.to_a))
end
def choose(*rest)
choose_p(*(rest.map {|x| [x, @one/rest.size]}))
end
def choose_p(*args)
savedstate = save()
x, prob = @amb.enum_choose(args)
restore(savedstate)
@currentprob *= prob
return x
end
def report(string)
if (@currentreport and @currentreport != '')
@currentreport = @currentreport.to_s
@currentreport += "\n#{string.to_s}"
else
@currentreport = string
end
end
def assert(cond)
failure unless cond
end
def ProbabilityTree.universe(one=nil)
ae = Amb.new
ptree = new(ae,one)
if (ae.choose(true,false))
yield ptree
ptree.send
else
return ptree.send
end
end
def ProbabilityTree.report(h)
rval = ""
h.keys.sort {|x,y| h[y] <=> h[x]}.each { |k|
rval += "#{k}\n==>\t#{h[k]}\n"
}
rval
end
def ProbabilityTree.runreport(one=nil)
report(universe(one) {|u| yield u})
end
end
# reference http://en.wikipedia.org/wiki/Three_cards_problem
puts 'Three-card problem:'
ProbabilityTree.runreport { |u|
card = u.choose('bw', 'ww', 'bb')
side = u.choose(0,1)
u.assert(card[side] == ?b);
u.report "Other side is #{card[1-side,1]}"
}.display
puts 'Three-card problem with someone who always shows you the black
side if possible:'
ProbabilityTree.runreport { |u|
card = u.choose('bw', 'ww', 'bb')
side = u.choose(0,1)
if card[side] != ?b and card[1-side] == ?b then
side = 1-side
end
u.assert(card[side] == ?b);
u.report "Other side is #{card[1-side,1]}"
}.display
# Monty Hall
require "rational"
# standard version
puts('===Monty Hall, classic version===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
guessdoor = u.choose(1,2,3)
remaining_doors = [1,2,3].select{|x|
x != treasuredoor and x != guessdoor }
showdoor = u.choose(*remaining_doors)
if (treasuredoor == guessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
# variation where the host doesn't know where the treasure is either,
# and sometimes accidentally reveals the treasure. In that case,
# the producers declare a do-over, and the segment is retaped.
puts('===Monty Hall with ignorant host and do-overs===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
guessdoor = u.choose(1,2,3)
remaining_doors = [1,2,3].select{|x| x != guessdoor}
showdoor = u.choose(*remaining_doors)
u.assert(treasuredoor != showdoor) # i.e. we're not a do-over
if (treasuredoor == guessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
# The lesson?
# use assert for stuff that "just happened". Pass a
# different list to choose for stuff that was deliberate.
# You and Bob both choose doors. Monty Hall tells one of you
# to go home, and opens the losers door, because they chose poorly.
# Should the remaining person switch doors or stick with their
# original guess?
puts('===Monty Hall with Bob===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
myguessdoor = u.choose(1,2,3)
bobguessdoor = u.choose(*([1,2,3].select{|i| i != myguessdoor}))
elimchoices = [[:me,myguessdoor],[:bob,bobguessdoor]]
elimchoices = elimchoices.select{|i| i[1] != treasuredoor}
elimchoice = u.choose(*elimchoices)
u.assert(elimchoice[0] == :bob)
if (treasuredoor == myguessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
# Same as above, but the host hates Bob and will eliminate
# him early whenever possible. Now, if Bob is eliminated
# what should you do?
puts('===Monty Hall hates Bob===')
ProbabilityTree.runreport(1.to_r) { |u|
treasuredoor = u.choose(1,2,3)
myguessdoor = u.choose(1,2,3)
bobguessdoor = u.choose(*([1,2,3].select{|i| i != myguessdoor}))
elimchoices = [[:me,myguessdoor],[:bob,bobguessdoor]]
elimchoices = elimchoices.select{|i| i[1] != treasuredoor}
elimchoice = nil
if elimchoices.size == 2 then
# If we're both still in the running to be eliminated, choose Bob
elimchoice = elimchoices[1]
else
# Choose whoever is left
elimchoice = elimchoices[0]
end
u.assert(elimchoice[0] == :bob)
if (treasuredoor == myguessdoor)
u.report "You should stay"
else
u.report "You should switch"
end
}.display
# This doesn't work
puts "Sum - naive"
ProbabilityTree.runreport(1.to_r) { |u|
sum = 0
3.times {
i = u.choose(0,1)
sum += i
}
u.report sum
}.display
# This does work
puts "Sum - store state"
ProbabilityTree.runreport(1.to_r) { |u|
sum = 0
3.times {
psum = sum
i = u.choose(0,1)
sum = psum + i
}
u.report sum
}.display
# This works too
puts "Sum - hidden store state"
ProbabilityTree.runreport(1.to_r) { |u|
sum = 0
3.times {
sum += u.choose(1,0)
}
u.report sum
}.display
# and this
puts "Sum - choose state"
ProbabilityTree.runreport(1.to_r) { |u|
sum = 0
3.times {
i,sum = u.choose([0,sum],[1,sum])
sum += i
}
u.report sum
}.display
# and this
puts "Sum - inject"
ProbabilityTree.runreport(1.to_r) { |u|
sum = (0 ... 3).inject(0) { |s,|
s + u.choose(0,1)
}
u.report sum
}.display
# A family has two children. One of them is a boy.
# What is a chance that the other is a boy?
puts "===Boy-Girl problem==="
ProbabilityTree.runreport(1.to_r) { |u|
child1 = u.choose
child2 = u.choose
u.assert(child1 == :boy || child2 == :boy)
if child1 == :boy and child2 == :boy then
u.report "Both children are boys"
else
u.report "Other child is a girl"
end
}.display