Contents
Deep Breakdown
What it tells me (Situation Awareness): The product rule for logarithms converts multiplication into addition: . This single identity is the algebraic foundation of why log-likelihood works in statistics. Products of probabilities are numerically catastrophic for large (floating-point underflow), but the log-likelihood is a stable sum. This appears in: maximum likelihood estimation (MLE) for any parametric distribution; the naive Bayes classifier (); joint probability of independent observations; entropy calculations ( can be rewritten via the rule); the log-normal distribution (where , so products of log-normal variables add on the log scale); and the chain rule of probability . The rule generalizes to any finite product: . The converse direction (sum of logs → log of product) is equally important for combining terms. Both directions require all arguments to be positive.
Flowchart
Real-world use
Where it is used
Maximum likelihood estimation: for IID data , the likelihood is . Taking the log: . Each application of the product rule converts one factor into a sum of log terms. Without the product rule, differentiation of the product is intractable for large . Naive Bayes: , assuming conditional independence (product of conditional probabilities → sum of log-conditionals).
Problem it solves
For Bernoulli observations with : the likelihood product is approximately , which is below the float64 minimum of and evaluates as exactly 0. The log-likelihood sum is a perfectly representable float.
How it solves the problem
Proof: let and . By definition, and . Then (exponent addition rule). Taking of both sides: . The key step is the exponent addition rule for the same base.
Example in a business
A data scientist builds a fraud detection model using naive Bayes. The posterior probability of fraud given 5 transaction features is . In production, they compute log-scores: , using the product rule. This avoids underflow and is additions instead of multiplications. Transactions with score > threshold are flagged.
The formula, piece by piece
Breaking the notation:
- : logarithm base , applied to the product .
- : the product of two positive numbers and .
- : log of the first factor alone.
- : log of the second factor alone.
- : addition — the core transformation: multiplication of arguments becomes addition of logs. Generalization to factors:
Common form in statistics (natural log of a joint likelihood):
Reverse direction: . Used to combine multiple log terms into a single argument before simplification. TRAP: only for products, not sums: . This is the most common log error in probability and statistics. Example: . TRAP: arguments must be positive: requires and . For probabilities , this is automatic but log-probabilities are then in , not .
The formula in words
": the log of a product equals the sum of the logs. Multiplication becomes addition. This is why log-likelihood works: products of many small probabilities that underflow to zero become sums of log-probabilities that are stable. In Python: np.log(probs).sum() implements directly. TRAP: this works only for products — log of a sum does not split."
Critical Concepts
- Why log-likelihood exists: the product rule is the single reason MLE is computationally feasible. Products of thousands of probabilities underflow in floating-point; sums of log-probabilities do not. This is not just a convenience — it is computationally necessary.
- Generalization to n factors: . This is the log-likelihood formula for IID observations. Any model using MLE relies on this identity.
- Log-normal products: if , their product is also log-normal, because , the sum of two normal random variables, which is normal.
- Naive Bayes uses the product rule: the conditional independence assumption means the class-conditional likelihood factors as a product. Taking log converts this to a sum of log-conditionals, which is both numerically stable and computationally instead of potentially catastrophic.
- TRAP: log of sum ≠ sum of logs: .
Common confusion in mixture models, where probabilities are summed () and incorrectly split. The log-sum-exp trick handles this case correctly.
Step-by-step calculation (tiny example)
Task: compute the log-likelihood of observing 3 heads and 2 tails with a coin where .
Step 1: Write the likelihood .
Step 2: Apply the product rule .
Step 3: Compute each term . .
Step 4: Sum .
Step 5: Verify . No underflow: is fine here, but with observations it would not be.
Code examples
Beginner example (Python)
# Product rule for logs: log_a(xy) = log_a(x) + log_a(y)
import math
import numpy as np
# Verify the product rule
print("Product rule verification: ln(xy) = ln(x) + ln(y)")
pairs = [(2.0, 3.0), (5.0, 7.0), (0.1, 0.9), (1.5, 4.0), (100.0, 0.01)]
print(f"{'x':>8} {'y':>8} {'ln(xy)':>10} {'ln(x)+ln(y)':>14} {'match':>6}")
for x, y in pairs:
direct = math.log(x * y)
rule_form = math.log(x) + math.log(y)
print(f"{x:>8.4f} {y:>8.4f} {direct:>10.6f} {rule_form:>14.6f} {str(abs(direct - rule_form) < 1e-10):>6}")
# Generalization: log-likelihood of IID Bernoulli observations
print("\nBernoulli log-likelihood: ln(L) = sum_i ln(p^x_i * (1-p)^(1-x_i))")
p = 0.4
obs = np.array([1, 0, 1, 1, 0, 1, 0, 0, 1, 1]) # 10 observations
ll_product = math.log(np.prod([p**x * (1 - p)**(1 - x) for x in obs]))
ll_sum = sum(x * math.log(p) + (1 - x) * math.log(1 - p) for x in obs)
print(f" Observations: {obs.tolist()}")
print(f" Log-likelihood (product then log): {ll_product:.6f}")
print(f" Log-likelihood (sum of logs): {ll_sum:.6f}")
print(f" Match: {abs(ll_product - ll_sum) < 1e-10}")
# TRAP: ln(x + y) ≠ ln(x) + ln(y)
x_val, y_val = 3.0, 5.0
print(f"\nTRAP: ln({x_val}+{y_val}) = ln({x_val+y_val}) = {math.log(x_val + y_val):.6f}")
print(f" ln({x_val}) + ln({y_val}) = ln({x_val*y_val}) = {math.log(x_val) + math.log(y_val):.6f}")
print(f" Difference: {abs(math.log(x_val + y_val) - (math.log(x_val) + math.log(y_val))):.6f}")
# Underflow demonstration
print("\nNumerical stability: products vs log-sums")
n = 500
p_val = 0.3
print(f" {n} observations with p={p_val}")
product = p_val**n
log_sum = n * math.log(p_val)
print(f" Direct product p^n: {product} (underflows to 0: {product == 0.0})")
print(f" Log-sum n*ln(p): {log_sum:.4f} (no underflow)")Beginner example (R)
# Product rule in R: log_a(xy) = log_a(x) + log_a(y)
# Verify
cat("Product rule verification: ln(xy) = ln(x) + ln(y)\n")
pairs <- list(c(2,3), c(5,7), c(0.1,0.9), c(1.5,4))
cat(sprintf("%-8s %-8s %-10s %-14s %s\n", "x", "y", "ln(xy)", "ln(x)+ln(y)", "match"))
for (p in pairs) {
x <- p[1]; y <- p[2]
direct <- log(x * y)
rule_form <- log(x) + log(y)
cat(sprintf("%-8.4f %-8.4f %-10.6f %-14.6f %s\n", x, y, direct, rule_form, abs(direct - rule_form) < 1e-10))
}
# Bernoulli log-likelihood
p_hat <- 0.4
obs <- c(1, 0, 1, 1, 0, 1, 0, 0, 1, 1)
ll_sum <- sum(obs * log(p_hat) + (1 - obs) * log(1 - p_hat))
cat(sprintf("\nBernoulli log-likelihood (sum of logs): %.6f\n", ll_sum))
# Underflow demo
n_obs <- 500; p_val <- 0.3
product_val <- p_val^n_obs
log_sum_val <- n_obs * log(p_val)
cat(sprintf("\nUnderflow: p^%d = %g (zero: %s)\n", n_obs, product_val, product_val == 0))
cat(sprintf("Log-sum: n*ln(p) = %.4f (no underflow)\n", log_sum_val))Professional example (Python)
import numpy as np
import pandas as pd
import math
from typing import Callable
# Scenario: compare three models on a binary classification task using
# log-likelihood (product rule converts product of probs to sum of log-probs)
# Demonstrate the numerical stability advantage and accuracy comparison
np.random.seed(42)
N_SAMPLES = 1000
TRUE_P = 0.35 # true positive rate (Bernoulli parameter)
labels = np.random.binomial(1, TRUE_P, N_SAMPLES)
def log_likelihood_bernoulli(labels: np.ndarray, predicted_p: float) -> float:
"""
Compute Bernoulli log-likelihood using the product rule.
ln L(p) = sum_i [y_i * ln(p) + (1-y_i) * ln(1-p)]
Applies: ln(prod_i p^y_i * (1-p)^(1-y_i)) = sum_i [y_i*ln(p) + (1-y_i)*ln(1-p)]
Args:
labels: Binary outcome array (0 or 1).
predicted_p: Predicted probability (0 < p < 1).
Returns:
Log-likelihood value.
"""
if not (0 < predicted_p < 1):
raise ValueError("predicted_p must be strictly between 0 and 1")
log_p = math.log(predicted_p)
log_one_m_p = math.log(1 - predicted_p)
return float(np.sum(labels * log_p + (1 - labels) * log_one_m_p))
def product_rule_verification(labels: np.ndarray, p: float) -> dict:
"""
Verify that the product of probabilities equals exp(sum of log-probs).
Args:
labels: Binary outcome array.
p: Probability parameter.
Returns:
Dictionary with comparison data.
"""
log_probs = labels * math.log(p) + (1 - labels) * math.log(1 - p)
sum_log = float(log_probs.sum())
exp_sum_log = math.exp(sum_log) if sum_log > -700 else 0.0 # avoid underflow in exp
return {"sum_of_logs": sum_log, "exp_of_sum": exp_sum_log}
# Evaluate log-likelihood over a grid of p values
p_grid = np.linspace(0.1, 0.9, 200)
lls = np.array([log_likelihood_bernoulli(labels, p) for p in p_grid])
best_p = p_grid[np.argmax(lls)]
# Compare models
model_predictions = {
"true_p (oracle)": TRUE_P,
"MLE estimate": labels.mean(),
"overestimate": 0.6,
"underestimate": 0.15,
}
rows = []
for model_name, pred_p in model_predictions.items():
ll = log_likelihood_bernoulli(labels, pred_p)
verification = product_rule_verification(labels, pred_p)
rows.append({
"model": model_name,
"predicted_p": round(pred_p, 4),
"log_likelihood": round(ll, 4),
"rank": None,
})
results_df = pd.DataFrame(rows)
results_df["rank"] = results_df["log_likelihood"].rank(ascending=False).astype(int)
results_df = results_df.sort_values("rank")
print("Bernoulli log-likelihood: product rule ln(prod p_i) = sum ln(p_i)")
print("=" * 66)
print(f" True p: {TRUE_P:.4f}")
print(f" Sample mean: {labels.mean():.4f} (MLE estimate)")
print(f" Grid MLE: {best_p:.4f}")
print()
print("Model comparison (higher log-likelihood = better fit):")
print(results_df.to_string(index=False))
print(f"\nProduct rule verification (n={N_SAMPLES} observations):")
verif = product_rule_verification(labels, labels.mean())
print(f" sum of log-probs: {verif['sum_of_logs']:.4f}")
print(f" exp(sum) = actual product: {verif['exp_of_sum']} (underflows: {verif['exp_of_sum'] == 0.0})")
print(f" Direct product (float64 result): {math.prod(labels * labels.mean() + (1-labels)*(1-labels.mean()))}")Professional example (R)
# Product rule: ln(prod p_i) = sum ln(p_i)
set.seed(42)
N_SAMPLES <- 50
labels <- rbinom(N_SAMPLES, 1, 0.4)
p <- ifelse(labels == 1, mean(labels), 1 - mean(labels))
sum_of_logs <- sum(log(p))
exp_of_sum <- exp(sum_of_logs)
direct_product <- prod(p)
cat(sprintf("sum of log-probs: %.4f exp(sum) = %.6e direct product: %.6e\n", sum_of_logs, exp_of_sum, direct_product))Source: 1300 p. 20