Hello! In our previous lesson, we successfully translated the theory of Welford's algorithm into a practical, class-based Python implementation. You now have a tool that can compute mean and variance in a single, numerically stable pass.
This lesson continues our journey into the practical side of variance calculation. We will implement the two alternative algorithms we've discussed: the naive single-pass method and the more stable two-pass method. Our goal is to achieve this learning outcome: Implement the naive single-pass and two-pass variance algorithms for comparison.
By the end of this lesson, you will have a complete trio of functions, setting the stage for our final empirical test where we'll see these algorithms in action.
1. Implementing the "Naive" Single-Pass Algorithm
First, let's implement the algorithm that is most directly derived from the "textbook" formula for variance. As we discussed in the first module, this approach is fast but notoriously prone to numerical errors due to catastrophic cancellation.
The formula, often called the "computational formula" for sample variance, is:
To implement this, we need to iterate through the data just once, keeping track of three values:
- The number of data points, .
- The sum of the data points, .
- The sum of the squares of the data points, .
After the loop, we plug these three aggregates into the formula. The Wikipedia article on this topic provides excellent pseudocode for this exact algorithm.
Algorithms for calculating variance
The Wikipedia article on 'Algorithms for calculating variance' describes the single-pass naive algorithm and its shortcomings. We will use its pseudocode as a guide for our implementation.
Focus on the section titled 'Naïve algorithm'. Review the pseudocode provided, which outlines the exact steps we will take to implement this function.
Following that structure, here is a straightforward Python implementation. This function takes a list of numbers and returns the sample variance.
def naive_variance(data, sample=True):
"""
Calculates variance using the naive, single-pass algorithm.
This method is prone to numerical instability.
Args:
data (list or iterable): A list of numerical data.
sample (bool): If True, calculates sample variance (n-1 denominator).
Otherwise, calculates population variance (n denominator).
Returns:
float: The calculated variance.
"""
n = 0
sum_val = 0.0
sum_sq = 0.0
for x in data:
n += 1
sum_val += x
sum_sq += x**2
if n < 2:
return 0.0 # Or float('nan')
# Using the computational formula for variance
if sample:
variance = (sum_sq - (sum_val**2) / n) / (n - 1)
else:
variance = (sum_sq - (sum_val**2) / n) / n
return variance
This implementation is clean and efficient in terms of passes, but as we know, its reliance on the subtraction of two potentially very large and similar numbers (sum_sq and (sum_val**2) / n) is its fatal flaw.
2. Implementing the Two-Pass Algorithm
Next, we'll implement the two-pass algorithm. This method is a significant improvement in numerical stability over the naive approach because it avoids the large-number subtraction problem. It adheres more closely to the definitional formula for variance.
The process involves two distinct passes over the data:
- First Pass: Calculate the mean, .
- Second Pass: Calculate the sum of squared differences from that mean, .
The sample variance is then this sum divided by .
The primary drawback, of course, is the need to iterate over the data twice. This is inefficient if the dataset is massive and stored on slow media, and it's impossible for data streams that you can't store. However, for moderately sized datasets in memory, it serves as an excellent, stable baseline for comparison.
The same Wikipedia resource also provides a clear implementation of this algorithm.
Algorithms for calculating variance
Let's return to the Wikipedia article to review the two-pass approach. This will confirm our implementation strategy.
Read the section titled 'Two-pass algorithm'. The Python code provided there is a direct and effective implementation of this method.
Based on that logic, here is the Python function for the two-pass algorithm:
def two_pass_variance(data, sample=True):
"""
Calculates variance using the stable, two-pass algorithm.
Args:
data (list or iterable): A list of numerical data.
sample (bool): If True, calculates sample variance (n-1 denominator).
Otherwise, calculates population variance (n denominator).
Returns:
float: The calculated variance.
"""
n = len(data)
if n < 2:
return 0.0 # Or float('nan')
# Pass 1: Calculate the mean
mean = sum(data) / n
# Pass 2: Calculate the sum of squared differences
sum_sq_diff = sum((x - mean)**2 for x in data)
# Final calculation
if sample:
variance = sum_sq_diff / (n - 1)
else:
variance = sum_sq_diff / n
return variance
This function is more robust than our naive implementation and will be our "ground truth" or stable baseline when we later compare results.
Test your understanding!
Consider the simple dataset [2, 4, 6]. The true sample variance is 4.0.
- For the
naive_variancefunction, what would be the final values ofn,sum_val, andsum_sqjust before the variance is calculated? - For the
two_pass_variancefunction, what would be the value ofmeanafter the first pass, andsum_sq_diffafter the second pass?
Show answer
-
naive_variancetrace:- After iterating through
[2, 4, 6]: n= 3sum_val= 2 + 4 + 6 = 12.0sum_sq= 2² + 4² + 6² = 4 + 16 + 36 = 56.0- Calculation:
(56.0 - (12.0**2) / 3) / (3 - 1)=(56.0 - 144.0 / 3) / 2=(56.0 - 48.0) / 2=8.0 / 2=4.0.
- After iterating through
-
two_pass_variancetrace:- Pass 1:
mean= (2 + 4 + 6) / 3 = 12 / 3 = 4.0. - Pass 2:
sum_sq_diff= (2 - 4)² + (4 - 4)² + (6 - 4)² = (-2)² + 0² + 2² = 4 + 0 + 4 = 8.0. - Calculation:
8.0 / (3 - 1)=4.0.
- Pass 1:
Both correctly calculate the variance for this simple case. The problems arise with less forgiving datasets, as we will see soon.
Conclusion
In this lesson, we have successfully implemented the two key algorithms for comparison against Welford's method. You now have three distinct functions ready for our empirical analysis.
Key Takeaways:
- The naive single-pass algorithm is implemented by accumulating the count, sum, and sum of squares, then applying the computational formula. It's fast but numerically unstable.
- The two-pass algorithm is implemented by first computing the mean, then iterating again to compute the sum of squared differences. It's numerically stable but requires two passes over the data.
- You now have a complete toolkit: the
Welfordclass from the previous lesson, and thenaive_varianceandtwo_pass_variancefunctions from this one.
Preview of the Next Lesson:
With our algorithmic toolkit complete, we can now move to the most interesting part: demonstrating the failure of the naive algorithm. In the next lesson, we will construct a synthetic dataset specifically designed to cause catastrophic cancellation. This will set the stage for the final lesson, where we run all three algorithms on this dataset and compare their accuracy.