Drawing beta pdfs in (P)react & D3
introduction
Last fall I worked on an MVP for visualizing experiment results. The goal of the project was to give LaunchDarkly user’s a way to easily interpret results of a given treatment and quickly make a decision.
While we use both normal and beta distributions to display results, this article is focused on understanding beta probability density functions and how to render them with VisX, D3 and [P]react.
Understanding the Beta Distribution and Probability Density Functions (PDFs)
The Beta distribution is a family of continuous probability distributions defined on the interval [0, 1], parameterized by two positive shape parameters, alpha (α) and beta (β). These parameters determine the distribution’s shape, making the Beta distribution suitable for variables like proportions or percentages, such as conversion rates in A/B testing.
The Probability Density Function (PDF) of the Beta distribution describes the likelihood of a random variable assuming a value within a possible range. The betaPDF
function in the provided code calculates the PDF for the Beta distribution, determining the probability density at each x value according to the Beta distribution.
Generating a Beta Distribution with TypeScript
To calculate a beta distribution, we pass our two shape parameters, α and β, to our randomBeta
function. This generates the y domain inside our beta PDF. The x domain is linearly generated by creating n samples between the bounds of [0, 1].
type Datum = { x: number; y: number };
const domain = (n: number) => [...Array(n).keys()].map((x) => x / n);
const betaPDF = (x: number, alpha: number, beta: number, result: number) =>
(Math.pow(x, alpha - 1) * Math.pow(1 - x, beta - 1)) / result;
export const betaDistribution = (
samples: number,
alpha: number,
beta: number
): Datum[] => {
const betaFn = randomBeta(alpha, beta);
const result = betaFn();
return domain(samples).map((x) => ({
x,
y: betaPDF(x, alpha, beta, result),
}));
};
The domain
Function
The domain
function in the code is instrumental in generating a set of evenly spaced values between 0 and 1. These values serve as the x-coordinates for the Beta distribution plot. Specifically, for a given n
, the function produces an array of n
elements, each initialized with its index value, and then divides each by n
to ensure all values lie between 0 and 1.
Plotting the Distribution
The following code illustrates how to plot this distribution:
// Code for plotting the distribution
import { h } from "preact";
import { scaleLinear } from "@visx/scale";
import { AreaClosed } from "@visx/shape";
import { curveBasis } from "@visx/curve";
import { Group } from "@visx/group";
import { GridColumns } from "@visx/grid";
import { AxisBottom, AxisLeft } from "@visx/axis";
import { extent } from "d3-array";
type Datum = { x: number; y: number };
type ChartProps = {
width: number;
height: number;
data: Datum[];
};
export const DensityPlot = ({ width, height, data }: ChartProps) => {
// bounds
const margin = { top: 40, right: 40, bottom: 64, left: 64 };
const xMax = width - margin.left - margin.right;
const yMax = height - margin.top - margin.bottom;
const x = scaleLinear<number>({
range: [1 / data.length, xMax],
nice: true,
clamp: true,
});
const y = scaleLinear<number>({ range: [yMax, 0], nice: true, clamp: true });
const xDomain = extent(data, ({ x: q }) => q) as number[];
const yDomain = extent(data, ({ y: p }) => p) as number[];
x.domain(xDomain);
y.domain(yDomain);
return (
<svg width={width} height={height}>
<Group left={margin.left} top={margin.top}>
<GridColumns scale={x} height={yMax} />
<AreaClosed
curve={curveBasis}
data={data}
yScale={y}
fill={"rgb(230, 30, 113)"}
x={(d: Datum) => x(d.x)}
y={(d: Datum) => y(d.y)}
/>
<AxisLeft
scale={y}
tickValues={[]}
label="density"
labelClassName={styles.axisLabel}
labelOffset={20}
/>
<AxisBottom
scale={x}
top={yMax}
label={xLabel}
labelClassName={styles.axisLabel}
labelOffset={20}
/>
</Group>
</svg>
);
};
There’s a lot going on here. DensityPlot
is our charting component that renders an svg
. Within our component, we scale or x
and y
domains linearly) to generate their respective axis
. The distribution itself is rendered with the <AreaClosed />
component which draws a curve with the area filled in underneath.
Demo
If we combine our chart with Preact signals, we get an interactive chart where the alpha
, beta
, and sample-size
values can be adjusted to change the shape of the distribution.