Contents
Deep Breakdown
What it tells me (Situation Awareness): The Comparison Test (Direct Comparison) says: if you can bound a mystery series above by a known converging series, then the mystery series also converges. Conversely, if you bound it below by a known diverging series, it diverges. It's the most intuitive convergence test — you're literally squeezing the unknown between known quantities. In DA/DS: bounding approximation errors, proving gradient norm sequences converge, and analyzing tail sums in probability distributions.
Flowchart
Real-world use
Where it is used
When you can recognize that your series' terms are dominated by a simpler series (like a p-series or geometric series) term-by-term. Common targets for comparison: (p-series, converges iff p>1) and (geometric, converges iff |r|<1).
Problem it solves
Some series are hard to analyze directly. By finding a simpler bounding series, you can borrow its convergence/divergence property.
How it solves the problem
If for all n and converges to some finite L, then the partial sums of are bounded above by L and non-decreasing, so they must converge by the monotone convergence theorem.
Example in a business
Tail sum bound for distribution: Sum of probabilities beyond a threshold. If P(X = n) ≤ (0.9)^n for all n ≥ N₀, then the tail sum . This gives a computable upper bound on how much probability mass you're ignoring when truncating a distribution at N₀.
The formula, piece by piece
Upper comparison (convergence): for all , and converges converges. Lower comparison (divergence): for all , and diverges diverges.
- : Both series must have non-negative terms.
- : Termwise inequality (comparison).
- "For all ": The inequality only needs to hold eventually, not necessarily from n=1.
- Known series arsenal: p-series (: converges iff p>1), geometric (: converges iff |r|<1), harmonic (: diverges).
The formula in words
"If every term of a mystery series is no bigger than the corresponding term of a series we know converges, the mystery series also converges." "If every term is at least as big as a series we know diverges, it diverges too."
Critical Concepts
- Non-negative terms required: Direct comparison only works for non-negative term series. For signed series, use absolute convergence or alternating series test.
- Termwise inequality is strict: You need , not just "of similar magnitude" — that's the limit comparison test.
- The "wrong" direction is inconclusive: with diverging tells you nothing about .
- Standard comparison series: Know these by heart:
- converges (p=2 > 1) — use as upper bound - diverges (harmonic) — use as lower bound - converges (geometric) — use as upper bound
- Tightness: Comparison gives a bound on the sum too: .
Step-by-step calculation (tiny example)
Test:
Step 1: Identify comparison series For large n:
Step 2: State the inequality for all n ≥ 1.
Step 3: Check the comparison series is a p-series with p=2 > 1 → converges (to π²/6 ≈ 1.645).
Step 4: Conclude By comparison test: CONVERGES. Numerical check: — finite, confirmed.
Code examples
Beginner example (Python)
"""
Comparison Test: if a_n <= b_n and sum(b_n) converges → sum(a_n) converges.
Numerical verification with partial sums.
"""
import numpy as np
from typing import Callable
def comparison_test_numerical(
series_a: Callable[[np.ndarray], np.ndarray],
series_b: Callable[[np.ndarray], np.ndarray],
num_terms: int = 10_000,
) -> dict:
"""
Numerically check comparison test: verify a_n <= b_n and estimate sums.
Parameters
----------
series_a : the series under investigation (a_n)
series_b : the comparison series (b_n, should be known)
num_terms : how many terms to compute
Returns
-------
dict with: comparison_holds, partial_sum_a, partial_sum_b, ratio_at_large_n
"""
indices = np.arange(1, num_terms + 1, dtype=float)
a_vals = series_a(indices)
b_vals = series_b(indices)
comparison_holds = bool(np.all(a_vals <= b_vals + 1e-14))
partial_sum_a = float(np.sum(a_vals))
partial_sum_b = float(np.sum(b_vals))
return {
"comparison_holds": comparison_holds,
"partial_sum_a": partial_sum_a,
"partial_sum_b": partial_sum_b,
"upper_bound_valid": comparison_holds and partial_sum_b < np.inf,
}
# Example: 1/(n^2+1) compared to 1/n^2
result = comparison_test_numerical(
series_a=lambda n: 1.0 / (n**2 + 1.0),
series_b=lambda n: 1.0 / n**2,
)
print(f"1/(n^2+1) vs 1/n^2:")
print(f" a_n <= b_n everywhere: {result['comparison_holds']}")
print(f" Partial sum a (10k): {result['partial_sum_a']:.6f}")
print(f" Partial sum b (10k): {result['partial_sum_b']:.6f} (= pi^2/6 - 1 ≈ {np.pi**2/6-1:.6f})")
print(f" Upper bound valid: {result['upper_bound_valid']}")Beginner example (R)
# Comparison test: sum 1/(n^2 + n) vs sum 1/n^2
comparison_test <- function(a_func, b_func, series_name, n_terms = 10000) {
n_vals <- seq_len(n_terms)
a_vals <- a_func(n_vals)
b_vals <- b_func(n_vals)
comparison_holds <- all(a_vals <= b_vals + 1e-14)
sum_a <- sum(a_vals)
sum_b <- sum(b_vals)
cat(sprintf("%s:\n a_n<=b_n: %s | sum_a=%0.6f | sum_b=%0.6f\n", series_name, comparison_holds, sum_a, sum_b))
}
comparison_test(function(n) 1 / (n^2 + n), function(n) 1 / n^2, "1/(n^2+n) vs 1/n^2")
comparison_test(function(n) 1 / (2^n + n), function(n) 1 / 2^n, "1/(2^n+n) vs 1/2^n")Professional example (Python)
"""
Comparison test for bounding tail probability in a model's output distribution.
Business context: Fraud detection model outputs P(fraud | features).
We want to bound the total probability mass in the tail (scores above
threshold N0) using a geometric comparison, confirming the tail is negligible.
"""
import numpy as np
NUM_SCORE_BUCKETS: int = 1000
THRESHOLD_INDEX: int = 100 # we ignore scores above this bucket
DECAY_FACTOR: float = 0.97 # geometric decay of probability per bucket
def simulate_fraud_score_distribution(
num_buckets: int, decay: float, seed: int = 42,
) -> np.ndarray:
"""Simulate a fraud score PMF that decays geometrically in the tail."""
rng = np.random.default_rng(seed)
raw_probs = np.zeros(num_buckets)
# First 100 buckets: main mass
raw_probs[:100] = rng.dirichlet(np.ones(100)) * 0.85
# Remaining: geometric tail
tail_indices = np.arange(num_buckets - 100)
raw_probs[100:] = 0.15 * decay**tail_indices * (1.0 - decay)
return raw_probs / raw_probs.sum()
fraud_pmf = simulate_fraud_score_distribution(NUM_SCORE_BUCKETS, DECAY_FACTOR)
tail_probs = fraud_pmf[THRESHOLD_INDEX:]
tail_indices = np.arange(len(tail_probs), dtype=float)
# Comparison: tail_prob[k] <= 0.15 * DECAY_FACTOR^k (geometric bound)
geometric_bound = DECAY_FACTOR**tail_indices / (1.0 - DECAY_FACTOR) * fraud_pmf[THRESHOLD_INDEX]
comparison_holds = np.all(tail_probs <= geometric_bound + 1e-14)
actual_tail_mass = np.sum(tail_probs)
geometric_tail_bound = np.sum(geometric_bound)
print(f"Tail probability mass (buckets > {THRESHOLD_INDEX}): {actual_tail_mass:.6f}")
print(f"Geometric comparison bound : {geometric_tail_bound:.6f}")
print(f"Comparison holds (a_n <= b_n) : {comparison_holds}")
print(f"Decision: tail mass is {'NEGLIGIBLE' if actual_tail_mass < 0.01 else 'SIGNIFICANT'}")Source: 1300 p. 316