Visualizing Bayesian A/B Testing in JavaScript with Angular and Highcharts

  • ·
  • 6 min read

Angular · Highcharts · A/B Testing

When most people think of A/B testing, they think of p-values and confidence intervals. This is known as the frequentist approach. Michael Frasco at Convoy explains frequentist A/B testing very succinctly:

In frequentist A/B testing, we use p-values to choose between two hypotheses: the null hypothesis — that there is no difference between variants A and B — and the alternative hypothesis — that variant B is different. A p-value measures the probability of observing a difference between the two variants at least as extreme as what we actually observed, given that there is no difference between the variants. Once the p-value achieves statistical significance or we’ve seen enough data, the experiment is over.

For frequentist A/B tests, no data outside the current test is considered. Bayesian testing introduces the ability to consider prior information when deciding between variants. Another advantage of Bayesian A/B testing is that you don’t need to know your sample size ahead of time.

While I’ll do my best to explain everything that’s happening here, I’m not a statistician. If you have questions about why you would Bayesian methods to conduct A/B testing or the math behind it, I would suggest one of these articles. The scope of this article is to show developers how to run the calculations necessary to make decisions about your tests and visualize the results.

Getting started

First, we’ll create our Angular application:

npm install -g @angular/cli
ng new bayesian-testing
cd bayesian-testing

We will also need angular-highcharts and jStat so let’s run the following command from the root of our project:

npm install --save angular-highcharts highcharts jstat

To use Highcharts in our application, we will need to import it into the module where we plan to use it. If you’re lazy loading your routes, you’ll need to import it there. Since our example is simple and just uses the one route, we’ll import it into app.module.ts:

// app.module.ts

import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'

// import the module
import { ChartModule } from 'angular-highcharts'

import { AppComponent } from './app.component'

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    // add it to our application imports
    ChartModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Getting our data

Now, we need to model the posterior distribution using jStat’s beta distribution methods. The posterior distribution is a probability distribution that, given some data, shows how likely it is that the true value is in some window. The more data you have, the smaller your window will be. In order to plot this data, we will need to sample values from the beta distribution. The number of samples we draw is arbitrary. But, the more samples we have, the higher our graph’s resolution will be.

There are several ways to go about drawing the samples. One way is to create an array (filled with 0s to avoid errors) and use the indices in Array.map to get the value of x in the Beta distribution at the index value. Since we are measuring click or conversion rates in A/B testing, we’ll make sure the index is scaled to how many samples we’re drawing. That may sound complicated but here’s what it looks like:

// app.component.ts

private getProbabilityDist(successes: number, trials: number, samples = 1000): number[][] {
  return new Array(samples)
    .fill(0)
    .map((_, i) => [
      (i / samples) * 100,
      jStat.beta.pdf(i / samples, successes + 1, trials - successes + 1)
    ]);
}

That’s pretty much it! We now have a way to get x and y values for our probability distributions. Now we just need to give our user a way to update them and stick the values in a chart.

Let’s create a chart variable in our app.component.ts file and write a method that will assign a Highchart chart to it when our method is called.

import { Component } from '@angular/core'
import { Chart } from 'angular-highcharts'
import jStat from 'jstat'

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  public chart: Chart

  constructor() {
    // initialize the chart with some values
    this.createChart(1, 10, 2, 10)
  }

  // our method to update or create our chart
  public createChart(
    successesA: number,
    trialsA: number,
    successesB: number,
    trialsB: number
  ): void {
    this.chart = new Chart({
      // give our chart some pretty colors
      colors: ['#0266C8', '#F90101'],
      // remove marker dots from the line that gets created in our chart
      plotOptions: {
        area: {
          marker: {
            enabled: false,
          },
        },
      },
      title: {
        text: '',
      },
      xAxis: [
        {
          title: { text: 'CTR (%)' },
          labels: {
            format: '{value}%',
          },
        },
      ],
      yAxis: [
        {
          title: { text: 'Density' },
        },
      ],
      series: [
        {
          name: 'a',
          type: 'area',
          // get the x and y values for A
          data: this.getProbabilityDist(
            parseInt(successesA),
            parseInt(trialsA)
          ),
        },
        {
          name: 'b',
          type: 'area',
          // get the x and y values for B
          data: this.getProbabilityDist(
            parseInt(successesB),
            parseInt(trialsB)
          ),
        },
      ],
    })
  }

  private getProbabilityDist(
    successes: number,
    trials: number,
    samples = 1000
  ): number[][] {
    return new Array(samples)
      .fill(0)
      .map((_, i) => [
        (i / samples) * 100,
        jStat.beta.pdf(i / samples, successes + 1, trials - successes + 1),
      ])
  }
}

Now all that’s left to do is create our HTML file. We’ll give our user a way to change the values and update the chart by using input fields and binding values to them using template reference variables (e.g. #successesA):

<div [chart]="chart"></div>

<label> Successes for A <input #successesA value="1" type="number" /> </label>
<br />
<label> Trials for A <input #trialsA value="10" type="number" /> </label> <br />
<label> Successes for B <input #successesB value="2" type="number" /> </label>
<br />
<label> Trials for B <input #trialsB value="10" type="number" /> </label> <br />
<button
  (click)="createChart(successesA.value, trialsA.value, successesB.value, trialsB.value)"
>
  Update Chart
</button>

That’s all! We can certainly make our inputs prettier but we now have a way to visualize our Bayesian A/B test results. See below for a working example: