Sunday, August 23, 2026
HomeData ScienceBayesian Statistics for Data Science: A Practical Introduction (2026)

Bayesian Statistics for Data Science: A Practical Introduction (2026)

Table of Content

Most data scientists learn statistics through the frequentist lens — p-values, confidence intervals, hypothesis tests. But there’s another entire framework for statistical reasoning: Bayesian statistics. Rather than asking “how often would this result occur by chance?”, Bayesian statistics asks “given what I’ve observed, what should I believe?” This shift in framing often produces more intuitive answers and richer insights, especially when you have prior knowledge or limited data.

Bayes’ Theorem: The Foundation

A system of linear equations printed in black ink on white paper
Photo by Antoine Dautry on Unsplash

Bayes’ theorem relates prior beliefs to posterior beliefs after observing data. In its probability form: P(hypothesis | data) = P(data | hypothesis) × P(hypothesis) / P(data). In plain English: your updated belief equals your likelihood of seeing this data if the hypothesis is true, multiplied by your prior belief, normalised by the probability of seeing this data at all.

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# Classic example: Is this coin fair?
# Prior: believe the coin is fair (bias = 0.5) but uncertain
# We observe 7 heads out of 10 flips

# Grid approximation — simple Bayesian inference
bias_grid = np.linspace(0, 1, 1000)  # possible values of P(heads)

# Prior: uniform (all biases equally likely before seeing data)
prior = np.ones(len(bias_grid))
prior /= prior.sum()

# Likelihood: P(7 heads in 10 flips | bias) — binomial distribution
n_flips, n_heads = 10, 7
likelihood = stats.binom.pmf(n_heads, n_flips, bias_grid)

# Posterior = likelihood * prior (then normalise)
posterior = likelihood * prior
posterior /= posterior.sum()

# Summary statistics
posterior_mean = np.sum(bias_grid * posterior)
# 95% credible interval
cumulative = np.cumsum(posterior)
ci_lower = bias_grid[np.searchsorted(cumulative, 0.025)]
ci_upper = bias_grid[np.searchsorted(cumulative, 0.975)]

print(f"Posterior mean:        {posterior_mean:.3f}")
print(f"95% Credible Interval: [{ci_lower:.3f}, {ci_upper:.3f}]")
print("Interpretation: There is a 95% probability the true bias")
print(f"lies between {ci_lower:.2f} and {ci_upper:.2f}")

plt.figure(figsize=(10, 4))
plt.plot(bias_grid, prior, 'b--', label='Prior', alpha=0.6)
plt.plot(bias_grid, likelihood / likelihood.sum(), 'g-', label='Likelihood (scaled)')
plt.plot(bias_grid, posterior, 'r-', label='Posterior', linewidth=2)
plt.axvline(posterior_mean, color='red', linestyle=':', label=f'Posterior mean = {posterior_mean:.3f}')
plt.xlabel('Coin bias (P(heads))'); plt.ylabel('Probability density')
plt.title('Bayesian Coin Flip Analysis'); plt.legend()
plt.show()

Notice the key difference from frequentist output: the 95% credible interval has a direct probabilistic interpretation — “there is a 95% probability the parameter lies in this range.” A frequentist 95% confidence interval does NOT mean this (it means 95% of such intervals constructed this way would contain the true value).

Bayesian A/B Testing

Bayesian A/B testing is one of the most practically useful applications for product and growth data scientists. Unlike frequentist A/B tests (which require choosing sample size in advance and can’t be “peeked” at), Bayesian tests can be monitored continuously and give intuitive answers like “there’s an 87% probability that variant B is better than A”:

from scipy.stats import beta

def bayesian_ab_test(control_conversions, control_trials,
                     variant_conversions, variant_trials,
                     n_samples=100_000):
    # Bayesian A/B test for conversion rates.
    # Prior: Beta(1, 1) = uniform prior (no prior knowledge)
    # Posterior distributions
    # Beta(alpha, beta) is conjugate prior for binomial likelihood
    control_posterior = beta(
        a=1 + control_conversions,
        b=1 + (control_trials - control_conversions)
    )
    variant_posterior = beta(
        a=1 + variant_conversions,
        b=1 + (variant_trials - variant_conversions)
    )

    # Monte Carlo: sample from both posteriors
    control_samples = control_posterior.rvs(n_samples)
    variant_samples = variant_posterior.rvs(n_samples)

    # P(variant > control)
    prob_variant_better = (variant_samples > control_samples).mean()

    # Expected lift
    expected_lift = (variant_samples / control_samples - 1).mean() * 100

    # Credible interval on the difference
    diff_samples = variant_samples - control_samples
    ci = np.percentile(diff_samples, [2.5, 97.5])

    print(f"Control:  {control_conversions}/{control_trials} = {control_conversions/control_trials:.2%}")
    print(f"Variant:  {variant_conversions}/{variant_trials} = {variant_conversions/variant_trials:.2%}")
    print(f"P(variant > control): {prob_variant_better:.1%}")
    print(f"Expected lift: {expected_lift:.1f}%")
    print(f"95% CI on difference: [{ci[0]:.3f}, {ci[1]:.3f}]")

    return prob_variant_better

# Example: Button colour test
prob = bayesian_ab_test(
    control_conversions=450, control_trials=5000,
    variant_conversions=510, variant_trials=5000
)

Bayesian Inference with PyMC

A pencil sitting on top of a piece of paper
Photo by Rishi on Unsplash

For more complex models, PyMC provides a full probabilistic programming framework. You describe the model in terms of distributions, and PyMC uses Markov Chain Monte Carlo (MCMC) to sample from the posterior:

import pymc as pm
import numpy as np

# Bayesian linear regression
np.random.seed(42)
X = np.random.normal(0, 1, 100)
y = 2.5 * X + 0.8 + np.random.normal(0, 0.5, 100)  # true: slope=2.5, intercept=0.8

with pm.Model() as linear_model:
    # Priors
    slope     = pm.Normal('slope',     mu=0, sigma=10)
    intercept = pm.Normal('intercept', mu=0, sigma=10)
    sigma     = pm.HalfNormal('sigma', sigma=1)

    # Likelihood
    mu = slope * X + intercept
    y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)

    # Sample from posterior
    trace = pm.sample(2000, tune=1000, return_inferencedata=True, progressbar=False)

print(pm.summary(trace, var_names=['slope', 'intercept', 'sigma']))

Frequently Asked Questions

When should I use Bayesian statistics instead of frequentist?
Bayesian methods shine when: you have genuine prior knowledge to incorporate, you need to update beliefs incrementally as data arrives, you want direct probability statements about parameters, or your sample size is small. Frequentist methods work well for large samples and when you want results that don’t depend on prior specification.

What’s a conjugate prior?
A conjugate prior is one where the prior and posterior belong to the same distribution family. For example, a Beta prior with a binomial likelihood gives a Beta posterior. This allows analytical (no sampling required) computation of the posterior — which is why the Beta distribution is used for A/B testing conversion rates.

Is Bayesian inference always computationally expensive?
Not always. Conjugate models have closed-form solutions. Grid approximation works for one or two parameters. MCMC is needed for complex multi-parameter models and can be slow but is highly parallelisable. Variational inference (used in PyMC and TensorFlow Probability) is faster but approximate.

Leave feedback about this

  • Rating

Durgesh Kekare
Durgesh Kekarehttps://www.dataexpertise.in
Durgesh Kekare is a data science educator and founder of DataExpertise.in. With expertise in Python, machine learning, and analytics, he helps 10,000+ learners break into data careers.

Latest Posts

List of Categories