A Numerically Stable and Fast Implementation of Moving Averages and Variances
msum and mvar functions in qfeval-functions, a library we are developing.
- Moving sum: If we split the series into chunks of length \(w\), every window decomposes into at most two parts: a suffix of the previous chunk and a prefix of the current chunk. Cumulative sums let us compute the sums of all prefixes and suffixes of every chunk at once. Combining them gives the moving sum and moving average (§1).
- Moving variance (per-part statistics): For each part, instead of retaining only its sum, we retain a triple: its count, mean, and sum of squared deviations. We compute these statistics from deviations relative to an element of the part itself (the first or last value of the chunk). Because variance is invariant under translation, this keeps the error at the scale of local variation within the window without changing the result (§2.3).
- Moving variance (merging statistics): We combine the variance statistics of the two parts using the merge formula of Chan et al. to obtain the statistics for the complete window (§2.4).
Problem setting
rolling(w).
- \(O(T)\) computational cost: The running time should not depend on the window width \(w\). For daily data, long windows such as \(w = 250\) are commonly used.
- Vectorizable: The whole computation should consist of tensor operations (cumulative sums and elementwise operations), with no explicit sequential loop along the time axis. This allows efficient execution on GPUs and with SIMD.
- Numerically stable: The error should be governed by the scale of local variation within the window, not by the level, drift, or length of the series.
- Exact propagation of NaN and ±inf: A missing value or infinity should affect only the windows that contain it, and should not affect the results of windows that do not.
1. Moving sums and averages: chunked cumulative sums
1.1. Differencing a global cumulative sum loses accuracy as the series grows
1.2. Chunking: every window splits into at most two parts
1.3. From part sums to moving sums and moving averages
msum and ma functions in qfeval-functions use this approach.
2. Extending the method to moving variance
2.1. The sum-of-squares formula suffers cancellation when the data level is large
2.2. Stable alternatives sacrifice speed or vectorizability
unfold and applying var. In our environment, this approach was 8 to 300 times slower than the \(O(T)\) methods (§4). Because its running time is proportional to the window width, it becomes impractical when \(w\) reaches several hundred.
rolling uses this method. However, because the state at each time step depends on the previous state, the computation cannot be parallelized along the time axis. Here we assume batches of thousands of series processed in float32, so we instead use tensor operations that can also be parallelized along that axis.
2.3. Per-part statistics and local centering
- The error stays at the scale of the window: Every prefix contains the first element, and every suffix contains the last, so the reference value is always an element of the part itself (Figure 2). Because each part is a subset of the window, the magnitude of the deviations \(y\) cannot exceed the range of values within the window. Even when the series has a high level or drift, the computation involves only differences local to the window (Figure 3, §3.1).
- The effect of NaN and ±inf is confined exactly: When the reference value is NaN or infinite, it affects only parts that contain that value—that is, precisely the windows whose results should be NaN anyway (§3.2). If an aggregate such as the chunk mean were used instead, a single NaN in the chunk would affect windows that do not contain it.
2.4. Merging the two parts with the formula of Chan et al.
2.5. The full algorithm
- Prepend enough NaNs to make the padded length a multiple of \(w\), and split the result into chunks of length \(w\) (§1.2).
- Within each chunk, compute cumulative sums of \(y\) and \(y^2\), where \(y\) is the deviation from the first value. This yields \((n, \bar m, M)\) for every prefix.
- Similarly, compute reversed cumulative sums of deviations from the last value to obtain \((n, \bar m, M)\) for every suffix.
- For each window, use the merge formula to compute \(M_{A \cup B}\), then divide by \(w - \mathrm{ddof}\). Windows and parts can be matched by simply shifting indices, so this stage also uses only elementwise operations.
- The prepended NaNs automatically make the first \(w-1\) outputs NaN. If \(T < w\), every output is NaN.
mvar in qfeval-functions).
cumsum on parallel hardware.
2.6. A small numerical example
| Part | Reference value | Deviations \(y\) | \(n\) | \(\bar m\) | \(M\) | \(\mu\) |
|---|---|---|---|---|---|---|
| \(A = (48)\) | last value 48 | \((0)\) | 1 | 0 | 0 | 48 |
| \(B = (49, 51)\) | first value 49 | \((0, 2)\) | 2 | 1 | \(4 - 2^2/2 = 2\) | 50 |
3. Numerical stability properties
3.1. Why the error stays at the local scale of the window
3.2. Why the influence of NaN and ±inf is confined to the window
3.3. Measurements
| Case (float32) | Sum-of-squares formula | This method |
|---|---|---|
| Offset: relative error (median) | about \(9 \times 10^{4}\) (no significant digits remain) | \(6 \times 10^{-3}\) (input quantization error is the floor) |
| Offset: fraction of negative variances | 31% | 0 |
| Drift: relative error (median) | about \(72\) | \(1.3 \times 10^{-4}\) |
4. Speed
unfold + var), meanwhile, costs \(O(Tw)\), and its running time grows in proportion to the window width. The following results were measured in our environment using an Apple silicon CPU, PyTorch 2.7, and float32.
| Shape | Window | Sum-of-squares formula | Per-window recomputation | This method |
|---|---|---|---|---|
| (10000,) | 20 | 0.21 ms | 1.5 ms | 0.18 ms |
| (100, 10000) | 20 | 2.0 ms | 145 ms | 2.7 ms |
| (100, 10000) | 500 | 2.1 ms | 763 ms | 3.1 ms |
| (1000, 10000) | 500 | 21 ms | 7237 ms | 23 ms |
5. Comparison with related methods
sum, among others.
| Method | Cost | Vectorizable | Numerical stability |
|---|---|---|---|
| Sum-of-squares formula (two moving sums) | \(O(T)\) | Yes | Unstable (error factor \(\mu^2/\sigma^2\)) |
| Difference of a global cumulative sum | \(O(T)\) | Yes | Degrades in proportion to series length (§1.1) |
| Per-window recomputation | \(O(Tw)\) | Yes | Stable (\(O(\varepsilon w)\)) |
| Sequential add/remove (pandas, etc.) | \(O(T)\) | Not along the time axis | Mostly stable (depends on the removal implementation) |
| This method (chunking + local centering + merge) | \(O(T)\) | Yes | Stable (worst case \(O(\varepsilon w^2)\); independent of level and series length) |
6. Implementation notes
- The role of the prepended NaNs: Using NaNs as the padding needed to align chunk boundaries satisfies the output conventions—“the first \(w-1\) outputs are NaN” and “all outputs are NaN if \(T < w\)”—without special-case branches. Prepending at least \(w\) NaNs ensures that even the start of the real data has a preceding chunk.
- Handling ddof: We assume \(0 \le \mathrm{ddof} < w\). Typically it is \(1\) for the sample variance or \(0\) for the population variance.
- Moving average and moving standard deviation: The moving sum and moving average require only the chunk sums of §1. The moving standard deviation is obtained as the square root of the moving variance.
- Clamping negative values: In this method, \(M\) can become slightly negative because of rounding error, whereas the ordinary two-pass method guarantees nonnegativity. If strict nonnegativity is required—for example, before taking a square root—clamp the final result to a minimum of 0. No negative values were observed under the measurement conditions in §3.3.
7. Design limitations
- The error bound is larger than that of the two-pass method: The worst-case error is \(O(\varepsilon w^2)\), a bound \(w\) times larger than the \(O(\varepsilon w)\) bound of the per-window two-pass method. When last-digit accuracy matters more than speed, or when \(M\) must be guaranteed nonnegative, the two-pass method is preferable (§3.1, §6).
- Not suited to streaming: This method assumes batch computation in chunks. For low-latency processing of data that arrives one point at a time, Welford's method or the add/remove approach (§2.2) is more appropriate.
- Cannot ignore NaNs within a window: If a window contains a NaN, its result is NaN. Computing over only the non-NaN elements, as with pandas'
min_periods, would require a different construction that also tracks the number of valid elements. - Inputs must be floating-point numbers: Because the method prepends NaNs and computes deviations, integer series must first be converted to floating point.
8. Summary
- Moving sums obtained by differencing a global cumulative sum lose accuracy as the series grows. By splitting the series into chunks as long as the window, we ensure that every window consists of at most two parts: a suffix and a prefix. Because each part sum contains at most \(w\) terms, moving sums and averages can be computed stably.
- Computing moving variance with the sum-of-squares formula amplifies error in proportion to \(\mu^2/\sigma^2\). Stable per-window recomputation, meanwhile, costs \(O(Tw)\), and sequential updates cannot be parallelized along the time axis. Our method replaces the single sum retained for each part with its count, mean, and sum of squared deviations. By expressing these statistics as deviations from an element of the part itself, the entire computation uses only local differences; the two parts are then combined using the merge formula of Chan et al.
- The method simultaneously achieves \(O(T)\) cost independent of window width, an implementation using only tensor operations, error independent of the level, drift, and length of the series, and exact propagation of NaNs and infinities. In our measurements, it took 0.9 to 1.5 times as long as the sum-of-squares formula.
- The building blocks are the classical methods of Welford and Chan–Golub–LeVeque, published from the 1960s through the 1980s. Here we have shown how to combine them for tensor computation over moving windows. The implementation is available through the
msumandmvarfunctions in qfeval-functions.
9. References
- [1] B. P. Welford, "Note on a Method for Calculating Corrected Sums of Squares and Products," Technometrics 4(3), 419―420, 1962. (Welford's online update formula. Stable updates of the mean and sum of squared deviations under sequential insertion)
- [2] Tony F. Chan, Gene H. Golub, Randall J. LeVeque, "Updating Formulae and a Pairwise Algorithm for Computing Sample Variances," COMPSTAT 1982, 30―41, 1982. (The update formulas and pairwise algorithm by Chan–Golub–LeVeque. The original source of the merge formula in this article)
- [3] Tony F. Chan, Gene H. Golub, Randall J. LeVeque, "Algorithms for Computing the Sample Variance: Analysis and Recommendations," The American Statistician 37(3), 242―247, 1983. (Error analysis and recommendations for variance computation algorithms by the same authors, including the effect of shifting (centering))
- [4] Nicholas J. Higham, "Accuracy and Stability of Numerical Algorithms, Second Edition," Society for Industrial and Applied Mathematics, 2002. (The standard textbook on rounding error analysis. Error bounds for summation and variance computation)
Last Modified: July 16, 2026