Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 11

Comparing Welford, Naive Single-Pass, and Two-Pass Algorithm Accuracy

Recommended resources

Welcome to our final lesson on Welford's algorithm! Over the past few lessons, we have journeyed from the theoretical underpinnings of numerical stability to the practical implementation of three distinct variance calculation algorithms. You've implemented the naive single-pass and two-pass methods, derived and coded Welford's online algorithm, and, in our last session, crafted a function to generate datasets specifically designed to expose numerical weaknesses.

Today, we bring everything together in a final, conclusive experiment. Our goal is to empirically demonstrate the concepts we've learned by running our three algorithms on the unstable dataset and comparing their accuracy. This will provide a clear, practical answer to why an algorithm like Welford's is so crucial in computational statistics.

This lesson directly addresses the final learning outcome of the course: Compare the accuracy of the Welford, naive single-pass, and two-pass algorithms using the constructed dataset.

1. Preparing for the Experiment

We have all the necessary components. Let's briefly assemble our tools.

The Algorithms:
You have already implemented the Welford class. For completeness, here are the Python implementations of the naive single-pass and two-pass algorithms, which you can use for the experiment. These are based on the pseudocode we've seen and are ready to run.

# Naive single-pass algorithm
def naive_variance(data):
    n = len(data)
    if n < 2:
        return 0.0
    sum_x = sum(data)
    sum_x2 = sum(x**2 for x in data)
    # This is the 'textbook' formula we derived
    variance = (sum_x2 - (sum_x**2) / n) / (n - 1)
    return variance

# Stable two-pass algorithm
def two_pass_variance(data):
    n = len(data)
    if n < 2:
        return 0.0
    # First pass: calculate the mean
    mean = sum(data) / n
    # Second pass: sum the squared differences from the mean
    variance = sum((x - mean)**2 for x in data) / (n - 1)
    return variance

The Dataset Generator:
We will use the create_unstable_dataset function from our previous lesson to generate the data for our test.

import numpy as np

def create_unstable_dataset(n, shift, seed=42):
    """
    Generates a dataset designed to cause numerical instability.
    """
    np.random.seed(seed)
    base_data = np.random.rand(n)
    shifted_data = base_data + shift
    return shifted_data

2. A Preview of the Showdown

Before we run our own experiment with thousands of data points, let's look at a simple, well-documented example that illustrates exactly what we expect to happen. This will help you anticipate the results and confirm our understanding.

Algorithms for calculating variance

The Wikipedia article on variance algorithms provides a perfect, concise example of the naive algorithm's failure. Let's read through it to see how a simple shift in the data can lead to catastrophic errors.

Please read the section titled 'Example'. It starts with a small sample (4, 7, 13, 16) and shows how both the naive and two-pass algorithms work correctly. Then, it adds a large offset (10⁸ and 10⁹) and shows how the naive algorithm's result deteriorates catastrophically while the two-pass algorithm remains accurate.

This example demonstrates the core issue in a nutshell: when the data values are large, the two terms in the naive formula, and , become massive numbers that are nearly identical. Subtracting them leads to a huge loss of significant figures—this is catastrophic cancellation in action. The naive algorithm ends up calculating the variance of numerical noise rather than the data's actual variance.


3. The Main Event: Running the Comparison

Now, let's conduct the experiment ourselves. We will generate an unstable dataset and feed it to all three of our algorithms. We'll also use NumPy's built-in variance function (numpy.var) as a "ground truth" reference, as its implementation is numerically stable.

Below is the complete Python script to run the comparison. It includes the Welford class you built, the naive_variance and two_pass_variance functions, and the create_unstable_dataset generator.

import numpy as np

# --- ALGORITHM IMPLEMENTATIONS ---

class Welford:
    """Implements Welford's online algorithm for mean and variance."""
    def __init__(self):
        self.count = 0
        self.mean = 0.0
        self.M2 = 0.0

    def update(self, new_value):
        self.count += 1
        delta = new_value - self.mean
        self.mean += delta / self.count
        delta2 = new_value - self.mean
        self.M2 += delta * delta2

    def get_variance(self):
        if self.count < 2:
            return float('nan')
        return self.M2 / (self.count - 1) # Sample variance

def naive_variance(data):
    n = len(data)
    if n < 2: return 0.0
    sum_x = sum(data)
    sum_x2 = sum(x**2 for x in data)
    variance = (sum_x2 - (sum_x**2) / n) / (n - 1)
    return variance

def two_pass_variance(data):
    n = len(data)
    if n < 2: return 0.0
    mean = sum(data) / n
    variance = sum((x - mean)**2 for x in data) / (n - 1)
    return variance

# --- DATASET GENERATOR ---

def create_unstable_dataset(n, shift, seed=42):
    np.random.seed(seed)
    base_data = np.random.rand(n)
    shifted_data = base_data + shift
    return shifted_data

# --- EXPERIMENT SETUP ---

# Parameters
n_samples = 10000
shift = 1e9  # 1 billion

# Generate the "poison pill" dataset
data = create_unstable_dataset(n_samples, shift)

# --- RUN AND COMPARE ---

# 1. Naive Algorithm
var_naive = naive_variance(data)

# 2. Two-Pass Algorithm
var_two_pass = two_pass_variance(data)

# 3. Welford's Algorithm
welford_aggregator = Welford()
for x in data:
    welford_aggregator.update(x)
var_welford = welford_aggregator.get_variance()

# 4. Ground Truth (NumPy's stable implementation)
var_numpy = np.var(data, ddof=1) # ddof=1 for sample variance

# --- DISPLAY RESULTS ---

print("--- Variance Calculation Comparison ---")
print(f"Dataset: {n_samples} points from Uniform(0,1) + {shift:.0e}")
print("-" * 35)
print(f"Theoretical Variance of Uniform(0,1) is 1/12 = {1/12:.6f}")
print("-" * 35)
print(f"NumPy (Ground Truth): \t{var_numpy:.6f}")
print(f"Two-Pass Algorithm:   \t{var_two_pass:.6f}")
print(f"Welford's Algorithm:  \t{var_welford:.6f}")
print(f"Naive Algorithm:      \t{var_naive:.6f}")

Expected Output:

When you run this script, you should see something similar to this:

--- Variance Calculation Comparison ---
Dataset: 10000 points from Uniform(0,1) + 1e+09
-----------------------------------
Theoretical Variance of Uniform(0,1) is 1/12 = 0.083333
-----------------------------------
NumPy (Ground Truth):   0.083363
Two-Pass Algorithm:     0.083363
Welford's Algorithm:    0.083363
Naive Algorithm:        0.078125

(Note: Your naive algorithm result might be slightly different, zero, or even negative depending on your system's floating-point specifics, but it will be clearly wrong).


4. The Verdict

The results speak for themselves.

  • The naive algorithm fails. It produces a result that is significantly different from the true variance. The subtraction of two enormous, nearly-equal numbers has destroyed the precision of the calculation.
  • The two-pass algorithm succeeds. By first calculating the mean and then working with the small residual values , it completely avoids catastrophic cancellation and computes an accurate result.
  • Welford's algorithm also succeeds, producing the same accurate result as the two-pass method. It achieves this stability while maintaining the benefit of being a single-pass, online algorithm.

This experiment provides the definitive, practical evidence for everything we have learned. The theoretical danger of catastrophic cancellation is not just a mathematical curiosity; it is a real-world problem that can lead to incorrect results in common statistical calculations.

Test your understanding!

Imagine you are working with a data stream of sensor readings that represent temperature. The sensor normally outputs values in Kelvin, so the numbers are always large (e.g., ~300 K). The sensor is very precise, so the actual variance in the readings is tiny. You need to compute the running variance of these readings in real-time on a device with limited memory, meaning you cannot store all the data points.

Which of the three algorithms is the only suitable choice for this task, and why?

Show answer

Welford's algorithm is the only suitable choice.

Reasoning:

  1. Numerical Stability: The data has a large mean (~300) and small variance, which is the exact scenario where the naive algorithm fails. So, the naive algorithm is out.
  2. Memory/Processing Constraints: The problem requires computing a running variance on a data stream without storing all values. The two-pass algorithm is disqualified because it needs to see all the data to compute the mean (first pass) before it can even start calculating the sum of squared differences (second pass).
  3. Welford's Advantage: Welford's algorithm is both numerically stable (avoiding the catastrophic cancellation issue) and a single-pass, online algorithm. It can process one value at a time and update the variance without needing to store past values, making it perfect for this scenario.

Conclusion and Course Summary

Congratulations on completing this course! You have successfully navigated from the foundational concepts of numerical stability to the practical implementation and empirical validation of Welford's algorithm.

Key Takeaways from Our Journey:

  1. The Problem: The simple "textbook" formula for variance is numerically unstable. It is highly susceptible to catastrophic cancellation when the data's mean is much larger than its standard deviation.
  2. The Theory: You derived the update rules for the mean and the sum of squared differences () that form the core of Welford's algorithm. This showed you why it works by avoiding the subtraction of large, nearly-equal numbers.
  3. The Practice: You implemented and compared three different algorithms, culminating in today's experiment. This provided clear, empirical proof that numerically stable algorithms are not just an academic exercise but a practical necessity for accurate data analysis.

You now possess a deep understanding of Welford's algorithm, from its mathematical origins to its practical benefits. This knowledge is invaluable for any data scientist or engineer building robust data processing pipelines, especially in the context of streaming data or large datasets where both accuracy and efficiency are paramount.

Further Exploration:

If you wish to build on what you've learned, you could explore:

  • Higher-order statistics: The same principles can be extended to calculate skewness and kurtosis in a single pass. The Wikipedia resource we used, "Algorithms for calculating variance," briefly touches on this in the "Higher-order statistics" section.
  • Parallel algorithms: There are parallel versions of Welford's algorithm that allow you to compute statistics on separate chunks of data and then combine the results efficiently and accurately.
  • Weighted variance: The algorithm can be adapted to handle weighted data points, which is common in many statistical applications.

Thank you for your dedication throughout this course. You've mastered a subtle but powerful tool in the data science toolkit.