---
title: What it means to measure a possibility
bibliography: ../references.bib
jupyter: python3
---
::: {.callout-note}
The executable cells on this page use Python 3.14 syntax, matching the project configuration. The Quarto kernel is named `python3`; before rendering with execution, configure that kernelspec to point to a Python 3.14 environment.
:::
I said that a probability is a measurement of a possibility. We've now formalized what a possibility is in this context. Now let's turn to the measurement part.
The Kolmogorov axioms build the notion of a [probability measure](https://en.wikipedia.org/wiki/Probability_measure) from the more general concept of a [measure](https://en.wikipedia.org/wiki/Measure_(mathematics)). All a probability measure $\mathbb{P}$ is going to do is to map from some event in the event space (e.g. high vowel, high back vowel, etc.) to a non-negative real value–with values corresponding to higher probabilities. So it is a function $\mathbb{P}: \mathcal{F} \rightarrow \mathbb{R}_+$. This condition is the first of the Kolmogorov axioms.
1. $\mathbb{P}: \mathcal{F} \rightarrow \mathbb{R}_+$
You might be used to thinking of probabilities as being between $[0, 1]$. This property is a consequence of the two other axioms:
2. The probability of the entire sample space $\mathbb{P}(\Omega) = 1$ (the *assumption of unit measure*)
3. Given a countable collection of events $E_1, E_2, \ldots \in \mathcal{F}$ that is pairwise disjoint–i.e. $E_i \cap E_j = \emptyset$ for all $i \neq j$–$\mathbb{P}\left(\bigcup_i E_i\right) = \sum_i \mathbb{P}(E_i)$ (the *assumption of [$\sigma$-additivity](https://en.wikipedia.org/wiki/Sigma-additive_set_function)*)
```{python}
#| code-fold: true
#| code-summary: Define `FiniteMeasurableSpace`
from collections.abc import Callable, Iterable, Iterator
from itertools import chain, combinations
from functools import reduce
type SampleSpace = frozenset[str]
type Event = frozenset[str]
type SigmaAlgebra = frozenset[Event]
def powerset[T](iterable: Iterable[T]) -> Iterator[tuple[T, ...]]:
"""Compute the power set of an iterable.
See https://docs.python.org/3/library/itertools.html#itertools-recipes
Parameters
----------
iterable : Iterable
The set to take the power set of.
Returns
-------
Iterable
All subsets of the input as tuples.
"""
s = list(iterable)
return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
class FiniteMeasurableSpace:
"""A finite measurable space.
Parameters
----------
atoms : SampleSpace
The atoms of the space.
sigma_algebra : SigmaAlgebra
The sigma-algebra of the space.
"""
def __init__(self, atoms: SampleSpace, sigma_algebra: SigmaAlgebra) -> None:
self._atoms = atoms
self._sigma_algebra = sigma_algebra
self._validate()
def _validate(self) -> None:
if not isinstance(self._atoms, frozenset):
raise TypeError("The sample space must be a frozenset")
if not isinstance(self._sigma_algebra, frozenset):
raise TypeError("The event family must be a frozenset")
if not all(isinstance(event, frozenset) for event in self._sigma_algebra):
raise TypeError("Every event must be a frozenset")
if frozenset() not in self._sigma_algebra:
raise ValueError("The event family must contain the empty event")
if self._atoms not in self._sigma_algebra:
raise ValueError("The event family must contain the sample space")
for subset in self._sigma_algebra:
if not subset <= self._atoms:
raise ValueError("All events must be a subset of the atoms")
if not (self._atoms - subset) in self._sigma_algebra:
raise ValueError("The σ-algebra must be closed under complements")
for subsets in powerset(self._sigma_algebra):
subsets = list(subsets)
# reduce raises on empty iterables
if not subsets:
continue
union = frozenset(reduce(frozenset.union, subsets))
if union not in self._sigma_algebra:
raise ValueError(
"The σ-algebra must be closed under countable union"
)
intersection = frozenset(reduce(frozenset.intersection, subsets))
if intersection not in self._sigma_algebra:
raise ValueError(
"The σ-algebra must be closed under countable intersection"
)
@property
def atoms(self) -> SampleSpace:
"""The atoms of the space."""
return self._atoms
@property
def sigma_algebra(self) -> SigmaAlgebra:
"""The sigma-algebra of the space."""
return self._sigma_algebra
```
```{python}
#| code-fold: true
#| code-summary: Define `generate_sigma_algebra`
def generate_sigma_algebra(
atoms: SampleSpace,
family: SigmaAlgebra,
) -> SigmaAlgebra:
"""Generate a sigma-algebra from a family of sets.
Parameters
----------
atoms : SampleSpace
The sample space.
family : SigmaAlgebra
The family of sets from which to generate the sigma-algebra.
Returns
-------
SigmaAlgebra
The smallest sigma-algebra containing the family.
"""
if not all(event <= atoms for event in family):
raise ValueError("Every generator must be a subset of the sample space")
sigma_algebra = {frozenset(), atoms, *family}
old_sigma_algebra: set[Event] = set()
while sigma_algebra != old_sigma_algebra:
old_sigma_algebra = set(sigma_algebra)
for event in old_sigma_algebra:
sigma_algebra.add(atoms - event)
for subsets in powerset(old_sigma_algebra):
subsets = list(subsets)
if not subsets:
continue
union = reduce(frozenset.union, subsets)
sigma_algebra.add(union)
intersection = reduce(frozenset.intersection, subsets)
sigma_algebra.add(intersection)
return frozenset(sigma_algebra)
```
```{python}
#| code-fold: true
#| code-summary: Define `highness_backness_space`
emptyset = frozenset()
vowels = frozenset({'e', 'i', 'o', 'u', 'æ', 'ɑ', 'ɔ', 'ə', 'ɛ', 'ɪ', 'ʊ'})
# high v. nonhigh
high = frozenset({'i', 'u', 'ɪ', 'ʊ'})
nonhigh = vowels - high
f_highness = frozenset({
frozenset(emptyset),
frozenset(high), frozenset(nonhigh),
frozenset(vowels)
})
# back v. nonback
back = frozenset({'u', 'ʊ', 'o', 'ɔ', 'ɑ'})
nonback = vowels - back
f_backness = frozenset({
frozenset(emptyset),
frozenset(back), frozenset(nonback),
frozenset(vowels)
})
highness_space = FiniteMeasurableSpace(vowels, f_highness)
backness_space = FiniteMeasurableSpace(vowels, f_backness)
f_highness_backness = generate_sigma_algebra(
vowels,
f_highness | f_backness,
)
highness_backness_space = FiniteMeasurableSpace(vowels, f_highness_backness)
```
```{python}
from itertools import combinations
from math import isclose, isfinite
from numbers import Real
class ProbabilityMeasure:
"""A probability measure with finite support.
Parameters
----------
domain : FiniteMeasurableSpace
The domain of the probability measure.
measure : dict[Event, float]
The graph of the measure.
"""
def __init__(
self,
domain: FiniteMeasurableSpace,
measure: dict[Event, float],
) -> None:
self._domain = domain
self._measure = measure
self._validate()
def __call__(self, event: Event) -> float:
"""Return the probability of an event.
Parameters
----------
event : Event
The event to measure.
Returns
-------
float
The probability of the event.
"""
return self._measure[event]
def _validate(self) -> None:
expected_events = set(self._domain.sigma_algebra)
supplied_events = set(self._measure)
if supplied_events != expected_events:
missing = expected_events - supplied_events
extra = supplied_events - expected_events
raise ValueError(
f"Probability graph has missing keys {missing} and extra keys {extra}."
)
for event, mass in self._measure.items():
if isinstance(mass, bool) or not isinstance(mass, Real):
raise TypeError(f"Probability of {event} must be a real number.")
if not isfinite(float(mass)):
raise ValueError(f"Probability of {event} must be finite.")
if mass < 0:
raise ValueError(f"Probability of {event} must be nonnegative.")
if not isclose(
self._measure[self._domain.atoms],
1.0,
rel_tol=1e-9,
abs_tol=1e-12,
):
raise ValueError("The probability of the sample space must be 1.")
for events in powerset(self._domain.sigma_algebra):
events = list(events)
if not events:
continue
if not any(e1.intersection(e2) for e1, e2 in combinations(events, 2)):
prob_union = self._measure[reduce(frozenset.union, events)]
prob_sum = sum(self._measure[e] for e in events)
if not isclose(prob_union, prob_sum, rel_tol=1e-9, abs_tol=1e-12):
raise ValueError("The measure does not satisfy 𝜎-additivity.")
```
The validator checks the exact finite event family and the numerical boundary cases. The following counterexamples must all be rejected.
```{python}
#| code-fold: true
#| code-summary: Exercise finite-space and probability-measure validators
def assert_rejected(
exception: type[Exception],
constructor: Callable[[], object],
) -> None:
try:
constructor()
except exception:
pass
else:
raise AssertionError(f"Expected {exception.__name__}")
toy_atoms = frozenset({'a', 'b'})
toy_empty = frozenset()
toy_a = frozenset({'a'})
toy_b = frozenset({'b'})
assert_rejected(
ValueError,
lambda: FiniteMeasurableSpace(
toy_atoms,
frozenset({toy_atoms, toy_a, toy_b}),
),
)
assert_rejected(
ValueError,
lambda: FiniteMeasurableSpace(
toy_atoms,
frozenset({toy_empty, toy_a, toy_b}),
),
)
toy_space = FiniteMeasurableSpace(
toy_atoms,
frozenset({toy_empty, toy_atoms}),
)
ProbabilityMeasure(toy_space, {toy_empty: 0.0, toy_atoms: 1.0})
assert_rejected(
ValueError,
lambda: ProbabilityMeasure(toy_space, {toy_atoms: 1.0}),
)
assert_rejected(
ValueError,
lambda: ProbabilityMeasure(
toy_space,
{toy_empty: 0.0, toy_atoms: 1.0, toy_a: 0.0},
),
)
assert_rejected(
ValueError,
lambda: ProbabilityMeasure(
toy_space,
{toy_empty: -0.1, toy_atoms: 1.0},
),
)
assert_rejected(
ValueError,
lambda: ProbabilityMeasure(
toy_space,
{toy_empty: 0.0, toy_atoms: float('nan')},
),
)
```
One example of a probability measure for our measurable space $\langle \Omega, \mathcal{F}_\text{highness-backness}\rangle$ is the uniform measure: $\mathbb{P}(E) = \frac{|E|}{|\Omega|}$.
```{python}
measure_highness_backness = ProbabilityMeasure(
highness_backness_space,
{e: len(e)/len(highness_backness_space.atoms)
for e in highness_backness_space.sigma_algebra}
)
```
So why do probabilities end up between $0$ and $1$? We need two consequences of the axioms: $\mathbb{P}(\emptyset)=0$ and the fact that every probability lies in $[0,1]$. We'll establish them separately because the second uses the first.
First, fix the disjoint events $\Omega$ and $\emptyset$. Their union is $\Omega$, so finite additivity, which is the two-event case of $\sigma$-additivity, gives
$$
\mathbb{P}(\Omega)
=\mathbb{P}(\Omega\cup\emptyset)
=\mathbb{P}(\Omega)+\mathbb{P}(\emptyset).
$$
Subtracting the finite quantity $\mathbb{P}(\Omega)=1$ from both sides yields $\mathbb{P}(\emptyset)=0$.
Second, fix an arbitrary event $E\in\mathcal{F}$ and let $E^c=\Omega-E$. Closure under complementation ensures that $E^c\in\mathcal{F}$. The events $E$ and $E^c$ are disjoint, and their union is $\Omega$. Applying finite additivity again gives the **complement identity**
$$
1=\mathbb{P}(\Omega)
=\mathbb{P}(E\cup E^c)
=\mathbb{P}(E)+\mathbb{P}(E^c).
$$
Both terms on the right are nonnegative because the codomain of $\mathbb{P}$ is $\mathbb{R}_+$. Hence $\mathbb{P}(E)\leq1$; and nonnegativity also gives $\mathbb{P}(E)\geq0$. Since $E$ was arbitrary, the range of any probability measure is contained in $[0,1]$. It need not equal the whole interval: a probability measure on a finite sample space may attain only finitely many values.
(One reason the codomain of $\mathbb{P}$ is often specified as the more general $\mathbb{R}_+$–rather than $[0, 1]$ is to make salient the fact that probabilities are analogous to other kinds of measurements, like weight, height, temperature, etc.)
```{python}
class ProbabilityMeasure(ProbabilityMeasure):
def are_mutually_exclusive(self, *events: Event) -> bool:
"""Check whether events are pairwise disjoint.
Parameters
----------
*events : Event
The events to check.
Returns
-------
bool
True if no two events overlap.
"""
self._validate_events(events)
return not any(e1.intersection(e2) for e1, e2 in combinations(events, 2))
def _validate_events(self, events: Iterable[Event]) -> None:
for i, event in enumerate(events):
if event not in self._domain.sigma_algebra:
raise ValueError(f"event{i} is not in the event space.")
```
In our running example, the set of high vowels $H$ and the set of not high vowels $L$ are mutually exclusive events because $H \cap L = \emptyset$.
```{python}
#| colab: {base_uri: 'https://localhost:8080/'}
#| outputId: f1bc196d-1c02-43b7-d035-d778846f0800
measure_highness_backness = ProbabilityMeasure(
highness_backness_space,
{e: len(e)/len(highness_backness_space.atoms)
for e in highness_backness_space.sigma_algebra}
)
measure_highness_backness.are_mutually_exclusive(high, nonhigh)
```