Skip to content

Instantly share code, notes, and snippets.

@tophtucker
Last active January 28, 2018 23:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tophtucker/3497751d1ae936f92f3fbf00e17e4a54 to your computer and use it in GitHub Desktop.
Save tophtucker/3497751d1ae936f92f3fbf00e17e4a54 to your computer and use it in GitHub Desktop.
Bayes (WIP 2)

Instructions: Click and drag black labels along edges; click gray labels to switch to controlling those.

From a friend in business school: “1. What's the probability of a HS athlete going pro? 2. Suppose we know a pro athlete. What's the probability she was a college athlete?”

Probability tree

So I was thinking about my favorite intuitive illustrations and explanations of conditional probability and Bayes' theorem, e.g.

The Victor Powell / Setosa one in particular is unbeatably excellent, I think. But I wanted to try this. It feels more intutive to me that P(A) and P(B) should be perpendicular. In particular, it makes independence of A and B easy to see: the black and gray lines exactly overlap; P(A) = P(A|B).

When there's some dependence, I'm interested by the diagonal line you get between the midpoints of the P(B|A) line and the P(B|¬A) line. It's not shown by default cuz it got noisy, but it's the diagonal line you see in-between switching between black and gray parameter sets. I can imagine (and want to try) an alternate control scheme where you just have an intersection you can drag around inside the box and an angle for the diagonal line. That's equally expressive as this scheme, and sorta interesting, because the angle of the diagonal line is so related (somehow?) to the, uh, covariance or w/e.

One cool thing about this is that you can feel out which things are linear and which are not. The slope of the diagonal line is independent of P(A), which I would not have intuited. And P(A|B) and P(A|¬B) are nonlinear functions of P(A), P(B|A), and P(B|¬A), which I don't think I had any intuition about, but feels central to a lot of counterintuitive results of conditional probability questions.

As nice as it is to “feel out”, I want to be able to SEE any of those things I feel — spatialize the state space. Plot everything as a function of everything else, see the steepest slopes, marginal sensitivities, etc. A kind of phase space, idk. Ideally with the same visualization. Explode it along a third axis of all possible values of the current parameter... yessssssss that'd be so good, so doable. Whichever parameter you're currently holding, explode out all possible values along the z-axis, so you can see the nonlinear effects of dragging by dx.

I still want to make something that captures some feeling I have of weighing prior and posterior confidences, and the updating flowing one way or the other accordingly, almost hydraulically.

Discussion question ideas, like if I were trying to teach this

  • How to interpret positions of lines
  • P(¬A) and P(¬B) are not explicitly shown. What are they?
  • Why don't we need separate controls for "not A" and "not B"?
  • Why is it a square? Does it matter? What might other rectangles mean?
  • What does and doesn't change when you click a grayed-out probability?
  • Calculate the area of a segment and compare to the symbolic rules for probabilities. How do you interpret the areas?
  • Calculate an area as a fraction of another larger area. How would you interpret that fraction?
  • Something about dependence and independence of various variables (probabilities) wrt others
  • Something about linear vs nonlinear responses to other variables (probabilities)
  • Drag P(B|A) and watch P(B). Is there a maximum or minimum value of P(B) you can achieve? What is it? What about if you drag P(B|A) and watch P(A|B)?
  • Try to get P(A|B) to equal P(B|A). What has to be true? Can you get them to equal any other way, or is your solution unique?
  • Under some circumstances, these relationships simplify to something more intuitive or linear. Can you find examples? Why might those situations be confusing?
  • Consider P(B) is very small and P(A|¬B) is very big. Drag P(A|B) between 0 and 1 and in-between. As you move it, how does P(B|A) move? What does it move between, roughly? At the same speed? I.e., if you change P(A|B) by some amount — let's say 0.1 — does P(B|A) always change by roughly the same amount, no matter what P(A|B) is? What is that amount?

To-do

  • Interactions should maybe be more obvious... ghost cursor?
  • ✅ Label areas: P(A ⋀ B), P(A ⋀ ¬B), P(¬A ⋀ B), P(¬A ⋀ ¬B)
  • ✅ Smoothly transition from A|B lines to B|A lines (ghosting the past lines and transitioning solid ones to new spots through the diagonal)
  • Show derivations and definitions on hover
  • Add a hover-sensitive plain text summary sentence caption ("If B is true, then the probability of A is the probability of A and B divided by the probability of A...", highlighting the relevant quantities and areas, maybe color-coding)
  • Add covariance as a lever to fiddle with (closely related but not identical to the diagonal??)
  • Toggle independence of A and B (or total dependence?)
  • One day a more constructive writeup would be good...
  • Could you represent dependence by skewing the square into a rhombus? Like the angle between the P(A) and P(B) edges would vary between 90º (independent) and 0º (totally dependent).

See also

<!DOCTYPE html>
<meta charset="utf-8">
<style>
html, body, svg {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
rect {
fill: none;
stroke: black;
}
line.line2 {
stroke: black;
}
line.varline {
stroke: #ddd;
stroke-dasharray: 2,2;
}
text {
text-anchor: middle;
font-family: sans-serif;
/*transition: fill .2s;*/
}
text.derived {
fill: #ddd;
}
g.area text {
font-size: 12px;
}
div {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 2;
pointer-events: none;
cursor: wait;
}
</style>
<body>
<svg></svg>
<div></div>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var width = 300,
height = 300,
x = d3.scaleLinear().range([0,width]),
y = d3.scaleLinear().range([0,height])
var p_a = Math.random(),
p_b_given_a = Math.random(),
p_b_given_not_a = Math.random(),
p_b,
p_a_given_b,
p_a_given_not_b
var variables = [
{
name: "P(A)",
axis: 0,
side: 0,
level: 1,
derived: false,
derivation: () => (p_b * p_a_given_b) + ((1-p_b) * p_a_given_not_b),
value: function(_) { return arguments.length ? p_a = _ : p_a }
},
{
name: "P(B|A)",
axis: 1,
side: 0,
level: 0,
derived: false,
derivation: () => (p_a_given_b * p_b) / p_a,
value: function(_) { return arguments.length ? p_b_given_a = _ : p_b_given_a }
},
{
name: "P(B|¬A)",
axis: 1,
side: 1,
level: 0,
derived: false,
derivation: () => ((1-p_a_given_b) * p_b) / (1-p_a),
value: function(_) { return arguments.length ? p_b_given_not_a = _ : p_b_given_not_a }
},
{
name: "P(B)",
axis: 1,
side: 0,
level: 1,
derived: true,
derivation: () => (p_a * p_b_given_a) + ((1-p_a) * p_b_given_not_a),
value: function(_) { return arguments.length ? p_b = _ : p_b }
},
{
name: "P(A|B)",
axis: 0,
side: 0,
level: 0,
derived: true,
derivation: () => (p_b_given_a * p_a) / p_b,
value: function(_) { return arguments.length ? p_a_given_b = _ : p_a_given_b }
},
{
name: "P(A|¬B)",
axis: 0,
side: 1,
level: 0,
derived: true,
derivation: () => ((1-p_b_given_a) * p_a) / (1-p_b),
value: function(_) { return arguments.length ? p_a_given_not_b = _ : p_a_given_not_b }
}
]
var areas = [
{
name: "P(A ⋀ B)",
value: () => p_a * p_b_given_a,
coords: () => variables[0].derived ? [p_b/2, p_a_given_b/2] : [p_b_given_a/2, p_a/2]
},
{
name: "P(A ⋀ ¬B)",
value: () => p_a * (1 - p_b_given_a),
coords: () => variables[0].derived ? [(p_b+1)/2, p_a_given_not_b/2] : [(p_b_given_a+1)/2, p_a/2]
},
{
name: "P(¬A ⋀ B)",
value: () => (1 - p_a) * p_b_given_not_a,
coords: () => variables[0].derived ? [p_b/2, (p_a_given_b+1)/2] : [p_b_given_not_a/2, (p_a+1)/2]
},
{
name: "P(¬A ⋀ ¬B)",
value: () => (1 - p_a) * (1 - p_b_given_not_a),
coords: () => variables[0].derived ? [(p_b+1)/2, (p_a_given_not_b+1)/2] : [(p_b_given_not_a+1)/2, (p_a+1)/2]
}
]
var overlay = d3.select("div")
var svg = d3.select("svg")
.append("g")
.attr("transform", `translate(${innerWidth/2 - width/2}, ${innerHeight/2 - height/2})`)
var label = svg.selectAll("text.label")
.data(variables)
.enter()
.append("text")
.classed("label", true)
var line = svg.selectAll("line.varline")
.data(variables)
.enter()
.append("line")
.classed("varline", true)
var line2 = svg.selectAll("line.line2")
.data(variables.filter(d => d.level == 0))
.enter()
.append("line")
.classed("line2", true)
var areaLabel = svg.selectAll("g.area")
.data(areas)
.enter()
.append("g")
.classed("area", true)
areaLabel.append("text").classed("name", true).attr("y", "-.25em")
areaLabel.append("text").classed("value", true).attr("y", ".75em")
var rect = svg.append("rect")
function renderLine(selection) {
selection.each(function(d) {
var sel = d3.select(this)
.classed("derived", d.derived)
if(d.axis===0) {
// P(A), y-axis (horizontal lines, moving along vertical axis)
sel
.attr("x1", x(d.level ? -0.1 : d.side ? p_b : 0))
.attr("x2", x(d.level ? 1.1 : d.side ? 1 : p_b))
.attr("y1", y(d.value()))
.attr("y2", y(d.value()))
} else if(d.axis===1) {
// P(B), x-axis (vertical lines, moving along horizontal axis)
sel
.attr("x1", x(d.value()))
.attr("x2", x(d.value()))
.attr("y1", y(d.level ? -0.1 : d.side ? p_a : 0))
.attr("y2", y(d.level ? 1.1 : d.side ? 1 : p_a))
}
})
}
function renderLineFragment(context) {
selection = context.selection ? context.selection() : context
selection.each(function(d) {
var sel = d3.select(this)
if (context !== selection) {
sel = sel.transition(context)
}
if(d.axis===0) {
// P(A), y-axis (horizontal lines, moving along vertical axis)
sel
.attr("x1", x(d.side ? p_b : 0))
.attr("x2", x(d.side ? 1 : p_b))
.attr("y1", y(d.value()))
.attr("y2", y(d.value()))
} else if(d.axis===1) {
// P(B), x-axis (vertical lines, moving along horizontal axis)
sel
.attr("x1", x(d.value()))
.attr("x2", x(d.value()))
.attr("y1", y(d.side ? p_a : 0))
.attr("y2", y(d.side ? 1 : p_a))
}
})
}
function renderLineJoined(context) {
selection = context.selection ? context.selection() : context
selection.each(function(d) {
var sel = d3.select(this)
if (context !== selection) {
sel = sel.transition(context)
}
if(d.axis===0) {
// P(A), y-axis (horizontal lines, moving along vertical axis)
sel
.attr("x1", x(d.side ? p_b : 0))
.attr("x2", x(d.side ? 1 : p_b))
.attr("y1", y(p_a))
.attr("y2", y(p_a))
} else if(d.axis===1) {
// P(B), x-axis (vertical lines, moving along horizontal axis)
sel
.attr("x1", x(p_b))
.attr("x2", x(p_b))
.attr("y1", y(d.side ? p_a : 0))
.attr("y2", y(d.side ? 1 : p_a))
}
})
}
function renderLineDiagonal(context) {
selection = context.selection ? context.selection() : context
selection.each(function(d) {
var sel = d3.select(this)
if (context !== selection) {
sel = sel.transition(context)
}
if(d.axis===0) {
// P(A), y-axis (horizontal lines, moving along vertical axis)
var lineEq = getLineFunctionFromPoints(
[p_b / 2, p_a_given_b],
[(p_b + 1)/2, p_a_given_not_b]
)
sel
.attr("x1", x(d.side ? p_b : 0))
.attr("x2", x(d.side ? 1 : p_b))
.attr("y1", y(lineEq(d.side ? p_b : 0)))
.attr("y2", y(lineEq(d.side ? 1 : p_b)))
} else if(d.axis===1) {
// P(B), x-axis (vertical lines, moving along horizontal axis)
var lineEq = getLineFunctionFromPoints(
[p_a / 2, p_b_given_a],
[(p_a + 1)/2, p_b_given_not_a]
)
sel
.attr("x1", x(lineEq(d.side ? p_a : 0)))
.attr("x2", x(lineEq(d.side ? 1 : p_a)))
.attr("y1", y(d.side ? p_a : 0))
.attr("y2", y(d.side ? 1 : p_a))
}
})
}
function renderLabel(selection) {
selection.each(function(d) {
var sel = d3.select(this).text(`${d.name} = ${d.value().toFixed(2)}`)
.classed("derived", d.derived)
if(d.derived) {
sel
.style("cursor", "pointer")
.on("click", () => {
overlay.style("pointer-events", "all")
variables.forEach(d => d.derived = !d.derived)
line2.filter(d => d.derived).transition()
.duration(500)
.call(renderLineDiagonal)
.transition()
.duration(500)
.call(renderLineJoined)
line2.filter(d => !d.derived).transition()
.delay(500)
.duration(500)
.call(renderLineDiagonal)
.transition()
.duration(500)
.call(renderLineFragment)
.on("end", () => {
overlay.style("pointer-events", null)
render()
})
areaLabel
.transition()
.delay(500)
.duration(500)
.attr("transform", d => `translate(${x(d.coords()[0])}, ${y(d.coords()[1])})`)
label
.transition()
.delay(500)
.duration(500)
.style("fill", d => d.derived ? "#ddd" : "#000")
})
} else {
sel.on("click", null)
}
if(d.axis===0) {
// P(A), y-axis (vertical)
sel
.style("text-anchor", d.side ? "start" : "end")
.attr("x", d.side ? x(1) : 0)
.attr("y", y(d.value()))
.attr("dx", (d.side ? 1 : -1) * (2 * d.level + 1) + "em")
.attr("dy", ".25em")
if(!d.derived) {
sel
.style("cursor", "ns-resize")
.call(d3.drag().on("drag", function(d,i) {
var val = Math.max(Math.min(y.invert(d3.event.y),1),0)
d.value(val)
d3.select(this).attr("y", y(d.value()))
render()
}))
}
} else if(d.axis===1) {
// P(B), x-axis (horizontal)
sel
.style("text-anchor", "middle")
.attr("x", x(d.value()))
.attr("y", d.side ? y(1) : 0)
.attr("dx", 0)
.attr("dy", .4 + (d.side ? 1 : -1) * (2 * d.level + 1) + "em")
if(!d.derived) {
sel
.style("cursor", "ew-resize")
.call(d3.drag().on("drag", function(d,i) {
var val = Math.max(Math.min(x.invert(d3.event.x),1),0)
d.value(val)
d3.select(this).attr("x", x(d.value()))
render()
}))
}
}
})
}
render()
function render() {
label.data().filter(d => d.derived).forEach(d => d.value(d.derivation()))
// cov(X, Y) = E[XY] - E[X]E[Y]
var covariance = (p_a * p_b_given_a) - (p_a * p_b)
x = x.range([0,width])
y = y.range([0,height])
rect
.attr("width", width)
.attr("height", height)
label.call(renderLabel)
line.call(renderLine)
line2.filter(d => d.derived).call(renderLineJoined)
line2.filter(d => !d.derived).call(renderLineFragment)
areaLabel
.attr("transform", d => `translate(${x(d.coords()[0])}, ${y(d.coords()[1])})`)
.call(renderAreaLabel)
}
function renderAreaLabel(selection) {
selection.each(function(d) {
var sel = d3.select(this)
sel.select(".name").text(d.name)
sel.select(".value").text(`= ${d.value().toFixed(2)}`)
})
}
function getLineFunctionFromPoints(a,b) {
var m = (b[1]-a[1])/(b[0]-a[0])
return x => m*(x - a[0]) + a[1]
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment