codexindependent-and-dependent-probability-tables.py
independent-and-dependent-probability-tables.py
"""Joint laws versus products of marginals; tested with Python 3.14.4.
Uses the repository's matplotlib/numpy environment. Writes the PNG to CWD.
"""
from fractions import Fraction
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def fraction(value):
return str(Fraction(float(value)).limit_denominator(100))
def draw_table(ax, matrix, x_values, title):
"""Show every mass plus both marginals and the grand total."""
rows, columns = matrix.shape
ax.axis("off")
ax.set_title(title, fontsize=13, pad=10)
data = []
for i in range(rows):
data.append([fraction(v) for v in matrix[i]] + [fraction(matrix[i].sum())])
data.append([fraction(v) for v in matrix.sum(axis=0)] + ["1"])
table = ax.table(
cellText=data,
rowLabels=[f"X = {x}" for x in x_values] + ["P(Y = y)"],
colLabels=[f"Y = {y}" for y in range(columns)] + ["P(X = x)"],
cellLoc="center",
rowLoc="center",
bbox=[0.18, 0.03, 0.81, 0.86],
)
table.auto_set_font_size(False)
table.set_fontsize(12)
for (row, col), cell in table.get_celld().items():
cell.set_edgecolor("#c3ccd6")
if row == 0 or col == -1:
cell.set_facecolor("#edf1f6")
elif row == rows + 1 or col == columns:
cell.set_facecolor("#edf6ed")
else:
value = matrix[row - 1, col]
cell.set_facecolor(plt.cm.Blues(0.05 + 0.7 * value / 0.5))
cell.set_text_props(color="#182332")
def main():
independent = np.full((2, 2), 0.25)
identical = np.diag([0.5, 0.5])
squared = np.array([[0, 1 / 3], [1 / 3, 0], [0, 1 / 3]])
cases = [
(independent, [0, 1], "Independent fair bits: all four cells factor", 0),
(identical, [0, 1], "Same fair bit, Y = X: matching marginals, different joint law", 0.25),
(squared, [-1, 0, 1], "X uniform on {-1, 0, 1}, Y = X²: zero covariance, dependent", 0),
]
plt.rcParams.update({"font.size": 12, "text.color": "#182332"})
fig = plt.figure(figsize=(9.4, 9.2), facecolor="white")
grid = fig.add_gridspec(6, 2, height_ratios=[0.3, 1, 0.3, 1, 0.3, 1.2],
hspace=0.30, wspace=0.18)
for i, (joint, x_values, label, expected_covariance) in enumerate(cases):
marginal_x = joint.sum(axis=1)
marginal_y = joint.sum(axis=0)
product = np.outer(marginal_x, marginal_y)
assert np.isclose(joint.sum(), 1)
assert np.isclose(product.sum(), 1)
assert np.all(joint >= 0)
assert np.allclose(product.sum(axis=1), marginal_x)
assert np.allclose(product.sum(axis=0), marginal_y)
x = np.array(x_values)
y = np.arange(joint.shape[1])
covariance = np.sum(joint * x[:, None] * y[None, :]) - (x @ marginal_x) * (y @ marginal_y)
assert np.isclose(covariance, expected_covariance)
assert np.allclose(joint, product) == (i == 0)
heading = fig.add_subplot(grid[2 * i, :])
heading.axis("off")
heading.text(0.5, 0.5, label, ha="center", va="center", fontsize=12, weight="bold")
draw_table(fig.add_subplot(grid[2 * i + 1, 0]), joint, x_values, "Actual joint probabilities")
draw_table(fig.add_subplot(grid[2 * i + 1, 1]), product, x_values, "Product of the marginals")
fig.suptitle("Independence requires equality in every joint cell", fontsize=17, weight="bold", y=0.98)
fig.text(0.5, 0.02, "Blue: joint cells • Green: marginal totals • Each table sums to 1",
ha="center", fontsize=11)
fig.subplots_adjust(left=0.04, right=0.98, bottom=0.065, top=0.94)
fig.savefig(Path.cwd() / "independent-and-dependent-probability-tables.png",
dpi=100, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()