How to Calculate Probability with Combinations

Probability measures the likelihood of an event occurring, expressed as the ratio of favorable outcomes to total possible outcomes. When calculating these outcomes, you need to determine whether...

Key Insights

  • Combinations calculate the number of ways to select items when order doesn’t matter, forming the foundation for probability calculations in scenarios like card games, lotteries, and quality control sampling.
  • The probability of an event equals favorable combinations divided by total combinations—understanding this ratio lets you calculate exact odds for complex scenarios like poker hands or defect detection.
  • Use the complement rule (1 - P(none)) for “at least one” problems and built-in libraries like Python’s math.comb() to avoid integer overflow with large factorials while maintaining computational efficiency.

Introduction to Probability and Combinations

Probability measures the likelihood of an event occurring, expressed as the ratio of favorable outcomes to total possible outcomes. When calculating these outcomes, you need to determine whether order matters. If it doesn’t—like when selecting lottery numbers or dealing poker hands—you use combinations.

Consider drawing 5 cards from a deck. The hand {A♠, K♠, Q♠, J♠, 10♠} is the same royal flush regardless of the order you drew the cards. This is fundamentally different from a race where finishing 1st, 2nd, 3rd matters (that’s permutations). Combinations answer: “How many ways can I choose r items from n items when order is irrelevant?”

Real-world applications include lottery odds (choosing 6 numbers from 49), quality control (selecting 10 items from a batch of 1000 to inspect), and poker probabilities (dealing 5 cards from 52). Understanding combinations lets you calculate exact probabilities for these scenarios.

The Combination Formula

The combination formula calculates how many ways you can select r items from n total items:

C(n,r) = n! / (r!(n-r)!)

Where:

  • n = total number of items
  • r = number of items to select
  • ! = factorial (multiply all positive integers up to that number)

Let’s manually calculate C(5,2)—choosing 2 items from 5:

C(5,2) = 5! / (2!(5-2)!) = 120 / (2 × 6) = 120 / 12 = 10

Here’s a from-scratch implementation:

def factorial(n):
    """Calculate factorial of n"""
    if n < 0:
        raise ValueError("Factorial undefined for negative numbers")
    if n == 0 or n == 1:
        return 1
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

def combination(n, r):
    """Calculate C(n,r) - combinations of r items from n"""
    if r > n or r < 0:
        return 0
    if r == 0 or r == n:
        return 1
    
    # Optimization: C(n,r) = C(n, n-r), use smaller value
    r = min(r, n - r)
    
    return factorial(n) // (factorial(r) * factorial(n - r))

# Test
print(f"C(5,2) = {combination(5, 2)}")  # Output: 10
print(f"C(52,5) = {combination(52, 5)}")  # Output: 2,598,960

The second result tells us there are 2,598,960 possible 5-card poker hands.

Calculating Basic Probabilities with Combinations

The probability formula using combinations is:

P(event) = favorable combinations / total combinations

Example: What’s the probability of drawing exactly 2 aces from a 5-card hand?

  • Favorable: Choose 2 aces from 4, and 3 non-aces from 48
  • Total: All possible 5-card hands
def prob_two_aces():
    """Probability of exactly 2 aces in 5-card hand"""
    # Ways to choose 2 aces from 4
    aces = combination(4, 2)
    
    # Ways to choose 3 non-aces from 48
    non_aces = combination(48, 3)
    
    # Favorable outcomes
    favorable = aces * non_aces
    
    # Total possible 5-card hands
    total = combination(52, 5)
    
    probability = favorable / total
    
    return probability

print(f"P(exactly 2 aces) = {prob_two_aces():.6f}")  # 0.039929
print(f"About 1 in {1/prob_two_aces():.1f}")  # About 1 in 25.0

Let’s calculate the probability of a pair (exactly 2 cards of the same rank):

def prob_one_pair():
    """Probability of exactly one pair in 5-card hand"""
    # Choose 1 rank for the pair (13 options)
    # Choose 2 cards from 4 of that rank
    pair = 13 * combination(4, 2)
    
    # Choose 3 different ranks from remaining 12
    other_ranks = combination(12, 3)
    
    # Choose 1 card from each of those 3 ranks
    other_cards = 4 ** 3
    
    favorable = pair * other_ranks * other_cards
    total = combination(52, 5)
    
    return favorable / total

print(f"P(one pair) = {prob_one_pair():.6f}")  # 0.422569

Multiple Event Probabilities

“At least one” problems are easier using the complement rule: P(at least one) = 1 - P(none).

Example: What’s the probability of drawing at least one ace in a 5-card hand?

def prob_at_least_one_ace():
    """Probability of at least 1 ace in 5-card hand"""
    # Easier: 1 - P(no aces)
    no_aces = combination(48, 5)  # Choose 5 from 48 non-aces
    total = combination(52, 5)
    
    prob_no_aces = no_aces / total
    prob_at_least_one = 1 - prob_no_aces
    
    return prob_at_least_one

print(f"P(at least 1 ace) = {prob_at_least_one_ace():.6f}")  # 0.341297

Here’s a lottery calculator for matching k numbers:

def lottery_probability(total_numbers, numbers_drawn, numbers_matched):
    """
    Calculate probability of matching exactly k numbers in lottery
    
    Args:
        total_numbers: Total numbers in pool (e.g., 49)
        numbers_drawn: How many numbers are drawn (e.g., 6)
        numbers_matched: How many you want to match (e.g., 3)
    """
    # Ways to match k winning numbers
    matched = combination(numbers_drawn, numbers_matched)
    
    # Ways to choose remaining from non-winning numbers
    remaining = numbers_drawn - numbers_matched
    non_winning = total_numbers - numbers_drawn
    not_matched = combination(non_winning, remaining)
    
    favorable = matched * not_matched
    total = combination(total_numbers, numbers_drawn)
    
    return favorable / total

# Example: 6/49 lottery
for k in range(7):
    prob = lottery_probability(49, 6, k)
    odds = 1 / prob if prob > 0 else float('inf')
    print(f"Match {k} numbers: {prob:.8f} (1 in {odds:,.0f})")

Practical Applications

Quality control sampling is a critical application. If a batch contains defective items, what’s the probability your sample detects them?

def quality_control_probability(batch_size, defects, sample_size, min_defects_found):
    """
    Probability of finding at least min_defects in sample
    
    Args:
        batch_size: Total items in batch
        defects: Number of defective items in batch
        sample_size: Size of random sample
        min_defects_found: Minimum defects to trigger action
    """
    good_items = batch_size - defects
    total_outcomes = combination(batch_size, sample_size)
    
    # Calculate P(finding < min_defects), then use complement
    prob_insufficient = 0
    
    for k in range(min_defects_found):
        if k > defects or (sample_size - k) > good_items:
            continue
        
        # Ways to choose k defects and (sample_size - k) good items
        ways = combination(defects, k) * combination(good_items, sample_size - k)
        prob_insufficient += ways / total_outcomes
    
    return 1 - prob_insufficient

# Example: 1000 item batch, 50 defects, sample 20 items
prob = quality_control_probability(1000, 50, 20, 2)
print(f"P(finding ≥2 defects in sample) = {prob:.4f}")  # 0.4586

This tells you that if 5% of your batch is defective, sampling 20 items gives you only a 46% chance of catching at least 2 defects.

Using Built-in Libraries

Python’s math.comb() (Python 3.8+) handles large numbers efficiently:

import math
from scipy.special import comb as scipy_comb
import time

def benchmark_combination_methods(n, r):
    """Compare performance of different combination methods"""
    
    # Custom implementation
    start = time.perf_counter()
    result1 = combination(n, r)
    time1 = time.perf_counter() - start
    
    # math.comb
    start = time.perf_counter()
    result2 = math.comb(n, r)
    time2 = time.perf_counter() - start
    
    # scipy
    start = time.perf_counter()
    result3 = int(scipy_comb(n, r, exact=True))
    time3 = time.perf_counter() - start
    
    print(f"C({n},{r}) = {result2:,}")
    print(f"Custom: {time1*1000:.4f}ms")
    print(f"math.comb: {time2*1000:.4f}ms")
    print(f"scipy: {time3*1000:.4f}ms")

benchmark_combination_methods(100, 50)

Refactored poker probability using math.comb:

import math

def prob_one_pair_optimized():
    """Optimized one pair probability using math.comb"""
    pair = 13 * math.comb(4, 2)
    other_ranks = math.comb(12, 3)
    other_cards = 4 ** 3
    favorable = pair * other_ranks * other_cards
    total = math.comb(52, 5)
    return favorable / total

Common Pitfalls and Best Practices

Integer Overflow: Custom factorial implementations fail with large numbers. Use math.comb() for production code.

Combinations vs Permutations: Use combinations when order doesn’t matter (lottery numbers), permutations when it does (race positions).

Validation: Always validate inputs to prevent nonsensical results:

def safe_combination(n, r):
    """Combination with comprehensive input validation"""
    if not isinstance(n, int) or not isinstance(r, int):
        raise TypeError("n and r must be integers")
    
    if n < 0 or r < 0:
        raise ValueError("n and r must be non-negative")
    
    if r > n:
        raise ValueError("r cannot exceed n")
    
    return math.comb(n, r)

def safe_probability(favorable_comb, total_comb):
    """Calculate probability with validation"""
    if total_comb == 0:
        raise ValueError("Total combinations cannot be zero")
    
    if favorable_comb < 0 or favorable_comb > total_comb:
        raise ValueError("Favorable combinations must be between 0 and total")
    
    return favorable_comb / total_comb

# Usage
try:
    prob = safe_probability(
        safe_combination(4, 2) * safe_combination(48, 3),
        safe_combination(52, 5)
    )
    print(f"Probability: {prob:.6f}")
except (TypeError, ValueError) as e:
    print(f"Error: {e}")

For extremely large combinations where exact calculation is impractical, consider using Stirling’s approximation or Monte Carlo simulation. But for most real-world probability problems, math.comb() handles the computation efficiently and accurately.

Understanding combinations transforms probability from abstract theory into practical calculation. Whether you’re analyzing poker odds, designing quality control procedures, or building lottery systems, these techniques give you the tools to calculate exact probabilities and make data-driven decisions.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.