The empiricaldist API#
This notebook documents the most useful features of the empiricaldist
API.
Click here to run this notebook on Colab.
try:
import empiricaldist
except ImportError:
!pip install empiricaldist
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
A Pmf is a Series#
empiricaldist
provides Pmf
, which is a Pandas Series that represents a probability mass function.
from empiricaldist import Pmf
You can create a Pmf
in any of the ways you can create a Series
, but the most common way is to use from_seq
to make a Pmf
from a sequence.
The following is a Pmf
that represents a six-sided die.
d6 = Pmf.from_seq([1,2,3,4,5,6])
By default, the probabilities are normalized to add up to 1.
d6
probs | |
---|---|
1 | 0.166667 |
2 | 0.166667 |
3 | 0.166667 |
4 | 0.166667 |
5 | 0.166667 |
6 | 0.166667 |
But you can also make an unnormalized Pmf
if you want to keep track of the counts.
d6 = Pmf.from_seq([1,2,3,4,5,6], normalize=False)
d6
probs | |
---|---|
1 | 1 |
2 | 1 |
3 | 1 |
4 | 1 |
5 | 1 |
6 | 1 |
Or normalize later (the return value is the prior sum).
d6.normalize()
6
Now the Pmf is normalized.
d6
probs | |
---|---|
1 | 0.166667 |
2 | 0.166667 |
3 | 0.166667 |
4 | 0.166667 |
5 | 0.166667 |
6 | 0.166667 |
Properties#
In a Pmf
the index contains the quantities (qs
) and the values contain the probabilities (ps
).
These attributes are available as properties that return arrays (same semantics as the Pandas values
property)
d6.qs
array([1, 2, 3, 4, 5, 6])
d6.ps
array([0.16666667, 0.16666667, 0.16666667, 0.16666667, 0.16666667,
0.16666667])
Plotting PMFs#
Pmf
provides two plotting functions. bar
plots the Pmf
as a histogram.
def decorate_dice(title):
"""Labels the axes.
title: string
"""
plt.xlabel('Outcome')
plt.ylabel('PMF')
plt.title(title)
d6.bar()
decorate_dice('One die')

plot
displays the Pmf
as a line.
d6.plot()
decorate_dice('One die')

Selection#
The bracket operator looks up an outcome and returns its probability.
d6[1]
0.16666666666666666
d6[6]
0.16666666666666666
Outcomes that are not in the distribution cause a KeyError
d6[7]
You can also use parentheses to look up a quantity and get the corresponding probability.
d6(1)
0.16666666666666666
With parentheses, a quantity that is not in the distribution returns 0
, not an error.
d6(7)
0
Mutation#
Pmf
objects are mutable, but in general the result is not normalized.
d7 = d6.copy()
d7[7] = 1/6
d7
probs | |
---|---|
1 | 0.166667 |
2 | 0.166667 |
3 | 0.166667 |
4 | 0.166667 |
5 | 0.166667 |
6 | 0.166667 |
7 | 0.166667 |
d7.sum()
1.1666666666666665
d7.normalize()
1.1666666666666665
d7.sum()
1.0000000000000002
Statistics#
Pmf
overrides the statistics methods to compute mean
, median
, etc.
These functions only work correctly if the Pmf
is normalized.
d6 = Pmf.from_seq([1,2,3,4,5,6])
d6.mean()
3.5
d6.var()
2.9166666666666665
d6.std()
1.707825127659933
Sampling#
choice
chooses a random values from the Pmf, following the API of np.random.choice
d6.choice(size=10)
array([3, 4, 2, 4, 2, 3, 5, 4, 1, 5])
sample
chooses a random values from the Pmf
, with replacement.
d6.sample(n=10)
array([5., 3., 4., 3., 5., 3., 1., 2., 4., 3.])
CDFs#
empiricaldist
also provides Cdf
, which represents a cumulative distribution function.
from empiricaldist import Cdf
You can create an empty Cdf
and then add elements.
Here’s a Cdf
that represents a four-sided die.
d4 = Cdf.from_seq([1,2,3,4])
d4
probs | |
---|---|
1 | 0.25 |
2 | 0.50 |
3 | 0.75 |
4 | 1.00 |
Properties#
In a Cdf
the index contains the quantities (qs
) and the values contain the probabilities (ps
).
These attributes are available as properties that return arrays (same semantics as the Pandas values
property)
d4.qs
array([1, 2, 3, 4])
d4.ps
array([0.25, 0.5 , 0.75, 1. ])
Displaying CDFs#
Cdf
provides two plotting functions.
plot
displays the Cdf
as a line.
def decorate_dice(title):
"""Labels the axes.
title: string
"""
plt.xlabel('Outcome')
plt.ylabel('CDF')
plt.title(title)
d4.plot()
decorate_dice('One die')

step
plots the Cdf as a step function (which is more technically correct).
d4.step()
decorate_dice('One die')

Selection#
The bracket operator works as usual.
d4[1]
0.25
d4[4]
1.0
Evaluating CDFs#
Cdf
provides forward
and inverse
, which evaluate the CDF and its inverse as functions.
Evaluating a Cdf
forward maps from a quantity to its cumulative probability.
d6 = Cdf.from_seq([1,2,3,4,5,6])
d6.forward(3)
array(0.5)
forward
interpolates, so it works for quantities that are not in the distribution.
d6.forward(3.5)
array(0.5)
d6.forward(0)
array(0.)
d6.forward(7)
array(1.)
You can also call the Cdf
like a function (which it is).
d6(1.5)
array(0.16666667)
forward
can take an array of quantities, too.
def decorate_cdf(title):
"""Labels the axes.
title: string
"""
plt.xlabel('Quantity')
plt.ylabel('CDF')
plt.title(title)
qs = np.linspace(0, 7)
ps = d6(qs)
plt.plot(qs, ps)
decorate_cdf('Forward evaluation')

Cdf
also provides inverse
, which computes the inverse Cdf
:
d6.inverse(0.5)
array(3.)
quantile
is a synonym for inverse
d6.quantile(0.5)
array(3.)
inverse
and quantile
work with arrays
ps = np.linspace(0, 1)
qs = d6.quantile(ps)
plt.plot(qs, ps)
decorate_cdf('Inverse evaluation')

These functions provide a simple way to make a Q-Q plot.
Here are two samples from the same distribution.
cdf1 = Cdf.from_seq(np.random.normal(size=100))
cdf2 = Cdf.from_seq(np.random.normal(size=100))
cdf1.plot()
cdf2.plot()
decorate_cdf('Two random samples')

Here’s how we compute the Q-Q plot.
def qq_plot(cdf1, cdf2):
"""Compute results for a Q-Q plot.
Evaluates the inverse Cdfs for a
range of cumulative probabilities.
cdf1: Cdf
cdf2: Cdf
Returns: tuple of arrays
"""
ps = np.linspace(0, 1)
q1 = cdf1.quantile(ps)
q2 = cdf2.quantile(ps)
return q1, q2
The result is near the identity line, which suggests that the samples are from the same distribution.
q1, q2 = qq_plot(cdf1, cdf2)
plt.plot(q1, q2)
plt.xlabel('Quantity 1')
plt.ylabel('Quantity 2')
plt.title('Q-Q plot');

Here’s how we compute a P-P plot
def pp_plot(cdf1, cdf2):
"""Compute results for a P-P plot.
Evaluates the Cdfs for all quantities in either Cdf.
cdf1: Cdf
cdf2: Cdf
Returns: tuple of arrays
"""
qs = cdf1.index.union(cdf2.index)
p1 = cdf1(qs)
p2 = cdf2(qs)
return p1, p2
And here’s what it looks like.
p1, p2 = pp_plot(cdf1, cdf2)
plt.plot(p1, p2)
plt.xlabel('Cdf 1')
plt.ylabel('Cdf 2')
plt.title('P-P plot');

Mutation#
Cdf
objects are mutable, but in general the result is not a valid Cdf.
d4[5] = 1.25
d4
probs | |
---|---|
1 | 0.25 |
2 | 0.50 |
3 | 0.75 |
4 | 1.00 |
5 | 1.25 |
d4.normalize()
d4
probs | |
---|---|
1 | 0.2 |
2 | 0.4 |
3 | 0.6 |
4 | 0.8 |
5 | 1.0 |
Statistics#
Cdf
overrides the statistics methods to compute mean
, median
, etc.
d6.mean()
3.5
d6.var()
2.916666666666667
d6.std()
1.7078251276599332
Sampling#
choice
chooses a random values from the Cdf, following the API of np.random.choice
d6.choice(size=10)
array([1, 1, 3, 2, 3, 4, 4, 5, 2, 2])
sample
chooses a random values from the Cdf
, with replacement.
d6.sample(n=10)
array([4., 3., 2., 6., 5., 4., 1., 6., 1., 4.])
Arithmetic#
Pmf
and Cdf
provide add_dist
, which computes the distribution of the sum.
Here’s the distribution of the sum of two dice.
d6 = Pmf.from_seq([1,2,3,4,5,6])
twice = d6.add_dist(d6)
twice
probs | |
---|---|
2 | 0.027778 |
3 | 0.055556 |
4 | 0.083333 |
5 | 0.111111 |
6 | 0.138889 |
7 | 0.166667 |
8 | 0.138889 |
9 | 0.111111 |
10 | 0.083333 |
11 | 0.055556 |
12 | 0.027778 |
twice.bar()
decorate_dice('Two dice')
twice.mean()
6.999999999999998

To add a constant to a distribution, you could construct a deterministic Pmf
const = Pmf.from_seq([1])
d6.add_dist(const)
probs | |
---|---|
2 | 0.166667 |
3 | 0.166667 |
4 | 0.166667 |
5 | 0.166667 |
6 | 0.166667 |
7 | 0.166667 |
But add_dist
also handles constants as a special case:
d6.add_dist(1)
probs | |
---|---|
2 | 0.166667 |
3 | 0.166667 |
4 | 0.166667 |
5 | 0.166667 |
6 | 0.166667 |
7 | 0.166667 |
Other arithmetic operations are also implemented
d4 = Pmf.from_seq([1,2,3,4])
d6.sub_dist(d4)
probs | |
---|---|
-3 | 0.041667 |
-2 | 0.083333 |
-1 | 0.125000 |
0 | 0.166667 |
1 | 0.166667 |
2 | 0.166667 |
3 | 0.125000 |
4 | 0.083333 |
5 | 0.041667 |
d4.mul_dist(d4)
probs | |
---|---|
1 | 0.0625 |
2 | 0.1250 |
3 | 0.1250 |
4 | 0.1875 |
6 | 0.1250 |
8 | 0.1250 |
9 | 0.0625 |
12 | 0.1250 |
16 | 0.0625 |
d4.div_dist(d4)
probs | |
---|---|
0.250000 | 0.0625 |
0.333333 | 0.0625 |
0.500000 | 0.1250 |
0.666667 | 0.0625 |
0.750000 | 0.0625 |
1.000000 | 0.2500 |
1.333333 | 0.0625 |
1.500000 | 0.0625 |
2.000000 | 0.1250 |
3.000000 | 0.0625 |
4.000000 | 0.0625 |
Comparison operators#
Pmf
implements comparison operators that return probabilities.
You can compare a Pmf
to a scalar:
d6.lt_dist(3)
0.3333333333333333
d4.ge_dist(2)
0.75
Or compare Pmf
objects:
d4.gt_dist(d6)
0.25
d6.le_dist(d4)
0.41666666666666663
d4.eq_dist(d6)
0.16666666666666666
Interestingly, this way of comparing distributions is nontransitive.
A = Pmf.from_seq([2, 2, 4, 4, 9, 9])
B = Pmf.from_seq([1, 1, 6, 6, 8, 8])
C = Pmf.from_seq([3, 3, 5, 5, 7, 7])
A.gt_dist(B)
0.5555555555555556
B.gt_dist(C)
0.5555555555555556
C.gt_dist(A)
0.5555555555555556
Joint distributions#
Pmf.make_joint
takes two Pmf
objects and makes their joint distribution, assuming independence.
d4 = Pmf.from_seq(range(1,5))
d4
probs | |
---|---|
1 | 0.25 |
2 | 0.25 |
3 | 0.25 |
4 | 0.25 |
d6 = Pmf.from_seq(range(1,7))
d6
probs | |
---|---|
1 | 0.166667 |
2 | 0.166667 |
3 | 0.166667 |
4 | 0.166667 |
5 | 0.166667 |
6 | 0.166667 |
joint = Pmf.make_joint(d4, d6)
joint
probs | ||
---|---|---|
1 | 1 | 0.041667 |
2 | 0.041667 | |
3 | 0.041667 | |
4 | 0.041667 | |
5 | 0.041667 | |
6 | 0.041667 | |
2 | 1 | 0.041667 |
2 | 0.041667 | |
3 | 0.041667 | |
4 | 0.041667 | |
5 | 0.041667 | |
6 | 0.041667 | |
3 | 1 | 0.041667 |
2 | 0.041667 | |
3 | 0.041667 | |
4 | 0.041667 | |
5 | 0.041667 | |
6 | 0.041667 | |
4 | 1 | 0.041667 |
2 | 0.041667 | |
3 | 0.041667 | |
4 | 0.041667 | |
5 | 0.041667 | |
6 | 0.041667 |
The result is a Pmf
object that uses a MultiIndex to represent the values.
joint.index
MultiIndex([(1, 1),
(1, 2),
(1, 3),
(1, 4),
(1, 5),
(1, 6),
(2, 1),
(2, 2),
(2, 3),
(2, 4),
(2, 5),
(2, 6),
(3, 1),
(3, 2),
(3, 3),
(3, 4),
(3, 5),
(3, 6),
(4, 1),
(4, 2),
(4, 3),
(4, 4),
(4, 5),
(4, 6)],
)
If you ask for the qs
, you get an array of pairs:
joint.qs
array([(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (2, 1), (2, 2),
(2, 3), (2, 4), (2, 5), (2, 6), (3, 1), (3, 2), (3, 3), (3, 4),
(3, 5), (3, 6), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6)],
dtype=object)
You can select elements using tuples:
joint[1,1]
0.041666666666666664
You can get unnnormalized conditional distributions by selecting on different axes:
Pmf(joint[1])
probs | |
---|---|
1 | 0.041667 |
2 | 0.041667 |
3 | 0.041667 |
4 | 0.041667 |
5 | 0.041667 |
6 | 0.041667 |
Pmf(joint.loc[:, 1])
probs | |
---|---|
1 | 0.041667 |
2 | 0.041667 |
3 | 0.041667 |
4 | 0.041667 |
But Pmf
also provides conditional(i, val)
which returns the conditional distribution where variable i
has the value val
:
joint.conditional(0, 1)
probs | |
---|---|
1 | 0.166667 |
2 | 0.166667 |
3 | 0.166667 |
4 | 0.166667 |
5 | 0.166667 |
6 | 0.166667 |
joint.conditional(1, 1)
probs | |
---|---|
1 | 0.25 |
2 | 0.25 |
3 | 0.25 |
4 | 0.25 |
It also provides marginal(i)
, which returns the marginal distribution along axis i
joint.marginal(0)
probs | |
---|---|
1 | 0.25 |
2 | 0.25 |
3 | 0.25 |
4 | 0.25 |
joint.marginal(1)
probs | |
---|---|
1 | 0.166667 |
2 | 0.166667 |
3 | 0.166667 |
4 | 0.166667 |
5 | 0.166667 |
6 | 0.166667 |
Here are some ways of iterating through a joint distribution.
for q in joint.qs:
print(q)
(1, 1)
(1, 2)
(1, 3)
(1, 4)
(1, 5)
(1, 6)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(2, 5)
(2, 6)
(3, 1)
(3, 2)
(3, 3)
(3, 4)
(3, 5)
(3, 6)
(4, 1)
(4, 2)
(4, 3)
(4, 4)
(4, 5)
(4, 6)
for p in joint.ps:
print(p)
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
0.041666666666666664
for q, p in joint.items():
print(q, p)
(1, 1) 0.041666666666666664
(1, 2) 0.041666666666666664
(1, 3) 0.041666666666666664
(1, 4) 0.041666666666666664
(1, 5) 0.041666666666666664
(1, 6) 0.041666666666666664
(2, 1) 0.041666666666666664
(2, 2) 0.041666666666666664
(2, 3) 0.041666666666666664
(2, 4) 0.041666666666666664
(2, 5) 0.041666666666666664
(2, 6) 0.041666666666666664
(3, 1) 0.041666666666666664
(3, 2) 0.041666666666666664
(3, 3) 0.041666666666666664
(3, 4) 0.041666666666666664
(3, 5) 0.041666666666666664
(3, 6) 0.041666666666666664
(4, 1) 0.041666666666666664
(4, 2) 0.041666666666666664
(4, 3) 0.041666666666666664
(4, 4) 0.041666666666666664
(4, 5) 0.041666666666666664
(4, 6) 0.041666666666666664
for (q1, q2), p in joint.items():
print(q1, q2, p)
1 1 0.041666666666666664
1 2 0.041666666666666664
1 3 0.041666666666666664
1 4 0.041666666666666664
1 5 0.041666666666666664
1 6 0.041666666666666664
2 1 0.041666666666666664
2 2 0.041666666666666664
2 3 0.041666666666666664
2 4 0.041666666666666664
2 5 0.041666666666666664
2 6 0.041666666666666664
3 1 0.041666666666666664
3 2 0.041666666666666664
3 3 0.041666666666666664
3 4 0.041666666666666664
3 5 0.041666666666666664
3 6 0.041666666666666664
4 1 0.041666666666666664
4 2 0.041666666666666664
4 3 0.041666666666666664
4 4 0.041666666666666664
4 5 0.041666666666666664
4 6 0.041666666666666664
Copyright 2021 Allen Downey
BSD 3-clause license: https://opensource.org/licenses/BSD-3-Clause