Contents
, where is the joint distribution and , are its marginals
Deep Breakdown
What it tells me (Situation Awareness):
Mutual information (MI) measures how many bits (or nats) of uncertainty about disappear once I know . It is zero if and only if and are independent, it is symmetric (), it is never negative, and unlike Pearson correlation it captures any kind of dependence: linear, monotonic, U-shaped, categorical. Formally it is the KL divergence between the joint distribution and the product of the marginals, , i.e. "how far is reality from the world where the two variables ignore each other". The price is that it has no sign and no natural scale (it is bounded by ), so it is often normalized. In DA/DS: feature selection (rank features by MI with the target, mutual_info_classif), detecting non-linear relationships that correlation misses, comparing two clusterings (normalized MI, adjusted MI), choosing which survey question or event best predicts churn, decision-tree splits (information gain is MI between the split and the label), and representation learning (InfoNCE / contrastive losses maximize a lower bound on MI).
Flowchart
Real-world use
Where it is used
Feature screening before modelling (scikit-learn mutual_info_classif / mutual_info_regression); information gain in decision trees; normalized mutual information (NMI) to compare a clustering with ground-truth segments; selecting sensors or survey items; word co-occurrence statistics in NLP (pointwise MI, PMI); contrastive learning objectives for embeddings.
Problem it solves
Correlation only sees straight lines. A feature such as "hours since last login" can have a U-shaped relationship with churn (both brand-new and long-dormant users churn), so Pearson's is near zero even though the feature is highly informative. Also, many business variables are categorical (channel, device, plan), where correlation is not even defined.
How it solves the problem
MI compares the observed joint probabilities with what independence would predict, cell by cell. Every cell where the pair appears more (or less) often than contributes; the weighting by makes the result an expectation of the log-ratio, which is always . No assumption about the shape of the relationship is needed.
Example in a business
A CRM team wants to know which signal tells them most about whether a customer buys: "opened the campaign email" or "visited the pricing page". They build the 2×2 joint table of each signal with purchase, compute MI in bits, and prioritize the signal with the higher MI for the lead-scoring model. In the worked example below, email opening yields bits out of bits, so knowing it removes about 12% of the uncertainty about purchase.
The formula, piece by piece
- , : two random variables (e.g. email opened, purchase made)
- : joint probability of seeing and together
- , : marginal probabilities (row and column sums of the joint table)
- : what the joint probability would be if and were independent
- : pointwise mutual information (PMI) of one cell; positive if the pair co-occurs more than chance, negative if less
- : average PMI under the true joint distribution; individual cells can be negative but the total cannot
- base: base 2 gives bits, natural log gives nats ( nat bits)
- : equivalent form; entropy of minus the entropy that remains after observing (see Shannon Entropy)
- : equivalent form; divergence of the joint from independence (see Kullback-Leibler (KL) Divergence)
The formula in words
"Mutual information is the average surprise saved: for every combination of values, compare how often it really happens with how often it would happen if the two variables were unrelated, take the log of that ratio, and average it using the real frequencies." Tiny example: if and are two independent coin flips, every cell has , every log-ratio is , so . If is an exact copy of a fair coin , the cells are and bit: knowing tells me everything about .
Critical Concepts
- Non-negative and zero only at independence: follows from Gibbs' inequality on the KL form. exactly characterizes independence, something correlation cannot do.
- No sign, no direction: MI says "related", not "positively related". Look at the PMI of individual cells or at the conditional rates to see the direction.
- Upper bound and normalization: . Common normalizations: (uncertainty coefficient, "fraction of explained"), and NMI for clustering comparison.
- Estimation is the hard part: for discrete data use the plug-in estimate from counts (biased upward for small samples and many categories). For continuous data, binning is sensitive to bin choice; scikit-learn uses the k-nearest-neighbour (Kraskov) estimator.
- Information gain = MI: the information gain of a decision-tree split on feature is exactly estimated on the node's data.
- Pointwise MI (PMI): the single-cell term is used in NLP for word associations; it is noisy for rare pairs, which is why positive PMI (PPMI) clips negatives to 0.
Step-by-step calculation (tiny example)
10,000 customers. = opened the email (yes/no), = purchased (yes/no). Joint probabilities: , , , .
Step 1: Marginals , . , .
Step 2: Independence baseline ; ; ; .
Step 3: Cell contributions open, buy: open, no buy: no open, buy: no open, no buy:
Step 4: Sum bits ( nats).
Step 5: Put it on a scale bits. Uncertainty coefficient : opening the email removes about 12% of the uncertainty about purchase.
Code examples
Beginner example (Python)
import numpy as np
# Joint table: rows = opened email (yes, no), cols = purchased (yes, no)
joint = np.array([[0.10, 0.20],
[0.05, 0.65]])
p_x = joint.sum(axis=1) # [0.30, 0.70]
p_y = joint.sum(axis=0) # [0.15, 0.85]
independent = np.outer(p_x, p_y)
contrib = joint * np.log2(joint / independent)
mi_bits = contrib.sum()
h_y = -(p_y * np.log2(p_y)).sum()
print("Cell contributions (bits):\n", contrib.round(4))
print(f"I(X;Y) = {mi_bits:.4f} bits") # 0.0745
print(f"I(X;Y) = {mi_bits * np.log(2):.4f} nats") # 0.0516
print(f"H(Y) = {h_y:.4f} bits") # 0.6098
print(f"Fraction of purchase uncertainty removed: {mi_bits / h_y:.3f}") # 0.122Beginner example (R)
joint <- matrix(c(0.10, 0.20,
0.05, 0.65), nrow = 2, byrow = TRUE)
p_x <- rowSums(joint)
p_y <- colSums(joint)
independent <- outer(p_x, p_y)
mi_bits <- sum(joint * log2(joint / independent))
h_y <- -sum(p_y * log2(p_y))
cat(sprintf("I(X;Y) = %.4f bits\n", mi_bits)) # 0.0745
cat(sprintf("H(Y) = %.4f bits, ratio = %.3f\n", h_y, mi_bits / h_y))Professional example (Python)
import numpy as np
from sklearn.feature_selection import mutual_info_classif
from sklearn.metrics import mutual_info_score, normalized_mutual_info_score
rng = np.random.default_rng(42)
n = 5_000
# Synthetic churn data with three features:
# - days_inactive: U-shaped effect (new AND dormant users churn) -> correlation misses it
# - support_tickets: linear-ish effect
# - noise: unrelated
days_inactive = rng.uniform(0, 60, n)
support_tickets = rng.poisson(2, n)
noise = rng.normal(size=n)
logit = 0.004 * (days_inactive - 30) ** 2 - 2.0 + 0.35 * support_tickets
churn = (rng.uniform(size=n) < 1 / (1 + np.exp(-logit))).astype(int)
X = np.column_stack([days_inactive, support_tickets, noise])
names = ["days_inactive", "support_tickets", "noise"]
mi = mutual_info_classif(X, churn, discrete_features=[False, True, False], random_state=42)
pearson = [np.corrcoef(X[:, j], churn)[0, 1] for j in range(X.shape[1])]
print(f"Churn rate: {churn.mean():.3f}\n")
print(f"{'feature':<16}{'MI (nats)':>10}{'Pearson r':>11}")
for name, m, r in zip(names, mi, pearson):
print(f"{name:<16}{m:>10.4f}{r:>11.3f}")
# Discrete MI from counts + normalized MI between two segmentations
channel = rng.choice(["email", "ads", "organic"], size=n, p=[0.3, 0.5, 0.2])
segment_a = np.where(channel == "email", 0, np.where(channel == "ads", 1, 2))
flip = rng.uniform(size=n) < 0.2 # second segmentation agrees 80% of the time
segment_b = np.where(flip, rng.integers(0, 3, n), segment_a)
print(f"\nMI(channel, churn) from counts: {mutual_info_score(channel, churn):.5f} nats")
print(f"NMI(segment_a, segment_b): {normalized_mutual_info_score(segment_a, segment_b):.3f}")Source: domain knowledge