Why use a Binning Analysis?
As described in Markus Wallerberger (2019), Vinay Ambegaokar, Matthias Troyer (2010), Tony F. Chan, Gene H. Golub, Randall J. LeVeque (1983), Carsten Bauer (2020), and James Gubernatis, Naoki Kawashima, Philipp Werner (2016), the presence of correlations in a data stream generated in Markov Chain Monte Carlo simulations renders any measures of error severely underestimated.
Theoretical background
The naive approach is to calculate the mean
and the variance of the mean, var_of_mean
, for a given data stream $X$. For uncorrelated data, this becomes
\[\begin{aligned} \mathtt{mean}(X) &= \frac{1}{M} \sum_{i = 1}^M x_i \\ \mathtt{var\_of\_mean}(X) &= \frac{1}{M(M-1)} \sum_{i = 1}^M (x_i - \mathtt{mean}(X))^2. \end{aligned}\]
Fortunately, in the presence of correlations, the mean
doesn't change. Unfortunately, the var_of_mean
does. Indeed, one can show that it's given by
\[\mathtt{var\_of\_mean}(X) = \frac{1 + 2\tau_X}{M(M-1)} \sum_{i = 1}^M (x_i - \mathtt{mean}(X))^2,\]
where the as shown in the Figure within the Accumulator
type hierarchy. Clearly, the uncorrelated var_of_mean
is just increased by a factor of $R_X \equiv 1 + 2\tau_X$, where $\tau_X$ is defined as the integrated autocorrelation_time
of the data stream. The binning analysis provides a fast as an O(N)
method for calculating the autocorrelation_time
. It's also cheap, only requiring O(log N)
in RAM. For those who are unfamiliar with the term, normally one would calculate $\tau_X$ by
\[\tau_X = \sum_{i = 1}^M \sum_{i < j} \left[ x_ix_j - \mathtt{mean}(x)^2 \right],\]
which is an O(N^2)
summation and can suffer from numerical instabilities.
As one performs pairwise means in each binning level to construct the next highest, and then calculates the var_of_mean
for each level, one can see that it rises and then eventually saturates around a particular plateau
value. Normalizing by the original var_of_mean
calculation assuming the data stream is uncorrelated, one can then calculate $R_X$ in the $\ell^{\rm th}$ binning level as
\[R_X(\ell) = \frac{\mathtt{var\_of\_mean}(X^{(\ell)})}{\mathtt{var\_of\_mean}(X^{(0)})} = \frac{ m^{(\ell)} \mathtt{var}(X^{(\ell)}) }{ \mathtt{var}(X^{(0)}) },\]
where var
is the normal variance and $m^{(\ell)}$ is the bin size, or the number of original data values accumulated into a single data point at the $\ell^{\rm th}$ level.
Visualizing a Binning Analysis
The final result of the binning analysis, for sufficiently long data streams, will be to see a sigmoid
-like curve. We've wrapped up a fitting and plotting workflow into the plot_binning_analysis
function below to demonstrate how it works. Feel free to skip over to the Visualization Examples if you have your own workflow established.
using OnlineLogBinning, Plots, LaTeXStrings
pyplot()
# Return the fitted inflection point
sigmoid_inflection(fit) = fit.param[2] / fit.param[3]
# Define a plotting function with Plots.jl
# This function plots the RxValues, the Sigmoid fit,
# the maximum trustworthy level, and the fitted
# inflection point.
function plot_binning_analysis(bacc)
# Plot the RxValues irrespective of the trusting cutoff
all_levels, all_rxvals = levels_RxValues(bacc, false)
plt = plot( all_levels, all_rxvals;
label = "Binning Analysis",
xlabel = L"Bin Level $\ell$",
ylabel = L"$R_X(\ell)$",
markershape = :circle,
legend = :topleft )
# Fit only the trustworthy levels
levels, rxvals = levels_RxValues(bacc)
fit = fit_RxValues(levels, rxvals)
# Plot the sigmoid fit
plot!(plt, all_levels, sigmoid(all_levels, fit.param);
label = "Sigmoid Fit")
# Plot the maximum trustworthy level
vline!(plt, [ max_trustworthy_level(bacc[level = 0].num_bins) ];
ls = :dash, color = "gray",
label = "Maximum Trustworthy Level")
# Plot the fitted inflection point if it's positive
if sigmoid_inflection(fit) > zero(fit.param[2])
vline!(plt, [ sigmoid_inflection(fit) ];
ls = :dot, color = "red",
label = "Sigmoid Inflection Point")
end
return plt
end
[ Info: Installing matplotlib via the Conda matplotlib package...
[ Info: Running `conda install -q -y matplotlib` in root environment
Collecting package metadata (current_repodata.json): ...working... done
Solving environment: ...working... done
## Package Plan ##
environment location: /home/runner/.julia/conda/3
added / updated specs:
- matplotlib
The following packages will be downloaded:
package | build
---------------------------|-----------------
alsa-lib-1.2.6.1 | h7f98852_0 578 KB conda-forge
attr-2.5.1 | h166bdaf_1 69 KB conda-forge
brotli-1.0.9 | h166bdaf_7 18 KB conda-forge
brotli-bin-1.0.9 | h166bdaf_7 19 KB conda-forge
cycler-0.11.0 | pyhd8ed1ab_0 10 KB conda-forge
dbus-1.13.6 | h5008d03_3 604 KB conda-forge
expat-2.4.8 | h27087fc_0 187 KB conda-forge
fftw-3.3.10 |nompi_ha7695d1_103 6.5 MB conda-forge
font-ttf-dejavu-sans-mono-2.37| hab24e00_0 388 KB conda-forge
font-ttf-inconsolata-3.000 | h77eed37_0 94 KB conda-forge
font-ttf-source-code-pro-2.038| h77eed37_0 684 KB conda-forge
font-ttf-ubuntu-0.83 | hab24e00_0 1.9 MB conda-forge
fontconfig-2.14.0 | h8e229c2_0 305 KB conda-forge
fonts-conda-ecosystem-1 | 0 4 KB conda-forge
fonts-conda-forge-1 | 0 4 KB conda-forge
fonttools-4.35.0 | py39hb9d737c_0 1.8 MB conda-forge
freetype-2.12.1 | hca18f0e_0 884 KB conda-forge
gettext-0.19.8.1 | h73d1719_1008 3.6 MB conda-forge
glib-2.72.1 | h6239696_0 443 KB conda-forge
glib-tools-2.72.1 | h6239696_0 107 KB conda-forge
gst-plugins-base-1.20.3 | hf6a322e_0 2.8 MB conda-forge
gstreamer-1.20.3 | hd4edc92_0 2.0 MB conda-forge
icu-70.1 | h27087fc_0 13.5 MB conda-forge
jack-1.9.18 | h8c3723f_1002 643 KB conda-forge
jpeg-9e | h166bdaf_2 269 KB conda-forge
keyutils-1.6.1 | h166bdaf_0 115 KB conda-forge
kiwisolver-1.4.4 | py39hf939315_0 76 KB conda-forge
krb5-1.19.3 | h3790be6_0 1.4 MB conda-forge
lcms2-2.12 | hddcbb42_0 443 KB conda-forge
lerc-4.0.0 | h27087fc_0 275 KB conda-forge
libbrotlicommon-1.0.9 | h166bdaf_7 65 KB conda-forge
libbrotlidec-1.0.9 | h166bdaf_7 33 KB conda-forge
libbrotlienc-1.0.9 | h166bdaf_7 287 KB conda-forge
libcap-2.64 | ha37c62d_0 96 KB conda-forge
libclang-14.0.6 |default_h2e3cab8_0 127 KB conda-forge
libclang13-14.0.6 |default_h3a83d3e_0 10.6 MB conda-forge
libcups-2.3.3 | h3e49a29_2 4.5 MB conda-forge
libdb-6.2.32 | h9c3ff4c_0 23.3 MB conda-forge
libdeflate-1.13 | h166bdaf_0 79 KB conda-forge
libedit-3.1.20191231 | he28a2e2_2 121 KB conda-forge
libevent-2.1.10 | h9b69904_4 1.1 MB conda-forge
libflac-1.3.4 | h27087fc_0 474 KB conda-forge
libglib-2.72.1 | h2d90d5f_0 3.1 MB conda-forge
libiconv-1.16 | h516909a_0 1.4 MB conda-forge
libllvm14-14.0.6 | he0ac6c6_0 35.2 MB conda-forge
libogg-1.3.4 | h7f98852_1 206 KB conda-forge
libopus-1.3.1 | h7f98852_1 255 KB conda-forge
libpng-1.6.37 | h753d276_4 371 KB conda-forge
libpq-14.5 | hd77ab85_0 3.0 MB conda-forge
libsndfile-1.0.31 | h9c3ff4c_1 602 KB conda-forge
libtiff-4.4.0 | h0e0dad5_3 642 KB conda-forge
libtool-2.4.6 | h9c3ff4c_1008 511 KB conda-forge
libudev1-249 | h166bdaf_4 109 KB conda-forge
libvorbis-1.3.7 | h9c3ff4c_0 280 KB conda-forge
libwebp-base-1.2.4 | h166bdaf_0 404 KB conda-forge
libxcb-1.13 | h7f98852_1004 391 KB conda-forge
libxkbcommon-1.0.3 | he3ba5ed_0 581 KB conda-forge
libxml2-2.9.14 | h22db469_4 771 KB conda-forge
matplotlib-3.5.3 | py39hf3d152e_1 7 KB conda-forge
matplotlib-base-3.5.3 | py39h700656a_1 7.4 MB conda-forge
munkres-1.1.4 | pyh9f0ad1d_0 12 KB conda-forge
mysql-common-8.0.30 | haf5c9bc_0 1.9 MB conda-forge
mysql-libs-8.0.30 | h28c427c_0 1.9 MB conda-forge
nspr-4.32 | h9c3ff4c_1 233 KB conda-forge
nss-3.78 | h2350873_0 2.1 MB conda-forge
openjpeg-2.5.0 | h7d73246_1 533 KB conda-forge
packaging-21.3 | pyhd8ed1ab_0 36 KB conda-forge
pcre-8.45 | h9c3ff4c_0 253 KB conda-forge
pillow-9.2.0 | py39hd5dbb17_2 44.9 MB conda-forge
ply-3.11 | py_1 44 KB conda-forge
portaudio-19.6.0 | h57a0ea0_5 131 KB conda-forge
pthread-stubs-0.4 | h36c2ea0_1001 5 KB conda-forge
pulseaudio-14.0 | h7f54b18_8 1.7 MB conda-forge
pyparsing-3.0.9 | pyhd8ed1ab_0 79 KB conda-forge
pyqt-5.15.7 | py39h18e9c17_0 6.2 MB conda-forge
pyqt5-sip-12.11.0 | py39h5a03fae_0 86 KB conda-forge
python-dateutil-2.8.2 | pyhd8ed1ab_0 240 KB conda-forge
qt-main-5.15.4 | ha5833f6_2 61.5 MB conda-forge
sip-6.6.2 | py39h5a03fae_0 495 KB conda-forge
toml-0.10.2 | pyhd8ed1ab_0 18 KB conda-forge
tornado-6.2 | py39hb9d737c_0 658 KB conda-forge
unicodedata2-14.0.0 | py39hb9d737c_1 498 KB conda-forge
xcb-util-0.4.0 | h166bdaf_0 20 KB conda-forge
xcb-util-image-0.4.0 | h166bdaf_0 24 KB conda-forge
xcb-util-keysyms-0.4.0 | h166bdaf_0 12 KB conda-forge
xcb-util-renderutil-0.3.9 | h166bdaf_0 15 KB conda-forge
xcb-util-wm-0.4.1 | h166bdaf_0 55 KB conda-forge
xorg-libxau-1.0.9 | h7f98852_0 13 KB conda-forge
xorg-libxdmcp-1.1.3 | h7f98852_0 19 KB conda-forge
zstd-1.5.2 | h6239696_4 448 KB conda-forge
------------------------------------------------------------
Total: 259.5 MB
The following NEW packages will be INSTALLED:
alsa-lib conda-forge/linux-64::alsa-lib-1.2.6.1-h7f98852_0
attr conda-forge/linux-64::attr-2.5.1-h166bdaf_1
brotli conda-forge/linux-64::brotli-1.0.9-h166bdaf_7
brotli-bin conda-forge/linux-64::brotli-bin-1.0.9-h166bdaf_7
cycler conda-forge/noarch::cycler-0.11.0-pyhd8ed1ab_0
dbus conda-forge/linux-64::dbus-1.13.6-h5008d03_3
expat conda-forge/linux-64::expat-2.4.8-h27087fc_0
fftw conda-forge/linux-64::fftw-3.3.10-nompi_ha7695d1_103
font-ttf-dejavu-s~ conda-forge/noarch::font-ttf-dejavu-sans-mono-2.37-hab24e00_0
font-ttf-inconsol~ conda-forge/noarch::font-ttf-inconsolata-3.000-h77eed37_0
font-ttf-source-c~ conda-forge/noarch::font-ttf-source-code-pro-2.038-h77eed37_0
font-ttf-ubuntu conda-forge/noarch::font-ttf-ubuntu-0.83-hab24e00_0
fontconfig conda-forge/linux-64::fontconfig-2.14.0-h8e229c2_0
fonts-conda-ecosy~ conda-forge/noarch::fonts-conda-ecosystem-1-0
fonts-conda-forge conda-forge/noarch::fonts-conda-forge-1-0
fonttools conda-forge/linux-64::fonttools-4.35.0-py39hb9d737c_0
freetype conda-forge/linux-64::freetype-2.12.1-hca18f0e_0
gettext conda-forge/linux-64::gettext-0.19.8.1-h73d1719_1008
glib conda-forge/linux-64::glib-2.72.1-h6239696_0
glib-tools conda-forge/linux-64::glib-tools-2.72.1-h6239696_0
gst-plugins-base conda-forge/linux-64::gst-plugins-base-1.20.3-hf6a322e_0
gstreamer conda-forge/linux-64::gstreamer-1.20.3-hd4edc92_0
icu conda-forge/linux-64::icu-70.1-h27087fc_0
jack conda-forge/linux-64::jack-1.9.18-h8c3723f_1002
jpeg conda-forge/linux-64::jpeg-9e-h166bdaf_2
keyutils conda-forge/linux-64::keyutils-1.6.1-h166bdaf_0
kiwisolver conda-forge/linux-64::kiwisolver-1.4.4-py39hf939315_0
krb5 conda-forge/linux-64::krb5-1.19.3-h3790be6_0
lcms2 conda-forge/linux-64::lcms2-2.12-hddcbb42_0
lerc conda-forge/linux-64::lerc-4.0.0-h27087fc_0
libbrotlicommon conda-forge/linux-64::libbrotlicommon-1.0.9-h166bdaf_7
libbrotlidec conda-forge/linux-64::libbrotlidec-1.0.9-h166bdaf_7
libbrotlienc conda-forge/linux-64::libbrotlienc-1.0.9-h166bdaf_7
libcap conda-forge/linux-64::libcap-2.64-ha37c62d_0
libclang conda-forge/linux-64::libclang-14.0.6-default_h2e3cab8_0
libclang13 conda-forge/linux-64::libclang13-14.0.6-default_h3a83d3e_0
libcups conda-forge/linux-64::libcups-2.3.3-h3e49a29_2
libdb conda-forge/linux-64::libdb-6.2.32-h9c3ff4c_0
libdeflate conda-forge/linux-64::libdeflate-1.13-h166bdaf_0
libedit conda-forge/linux-64::libedit-3.1.20191231-he28a2e2_2
libevent conda-forge/linux-64::libevent-2.1.10-h9b69904_4
libflac conda-forge/linux-64::libflac-1.3.4-h27087fc_0
libglib conda-forge/linux-64::libglib-2.72.1-h2d90d5f_0
libiconv conda-forge/linux-64::libiconv-1.16-h516909a_0
libllvm14 conda-forge/linux-64::libllvm14-14.0.6-he0ac6c6_0
libogg conda-forge/linux-64::libogg-1.3.4-h7f98852_1
libopus conda-forge/linux-64::libopus-1.3.1-h7f98852_1
libpng conda-forge/linux-64::libpng-1.6.37-h753d276_4
libpq conda-forge/linux-64::libpq-14.5-hd77ab85_0
libsndfile conda-forge/linux-64::libsndfile-1.0.31-h9c3ff4c_1
libtiff conda-forge/linux-64::libtiff-4.4.0-h0e0dad5_3
libtool conda-forge/linux-64::libtool-2.4.6-h9c3ff4c_1008
libudev1 conda-forge/linux-64::libudev1-249-h166bdaf_4
libvorbis conda-forge/linux-64::libvorbis-1.3.7-h9c3ff4c_0
libwebp-base conda-forge/linux-64::libwebp-base-1.2.4-h166bdaf_0
libxcb conda-forge/linux-64::libxcb-1.13-h7f98852_1004
libxkbcommon conda-forge/linux-64::libxkbcommon-1.0.3-he3ba5ed_0
libxml2 conda-forge/linux-64::libxml2-2.9.14-h22db469_4
matplotlib conda-forge/linux-64::matplotlib-3.5.3-py39hf3d152e_1
matplotlib-base conda-forge/linux-64::matplotlib-base-3.5.3-py39h700656a_1
munkres conda-forge/noarch::munkres-1.1.4-pyh9f0ad1d_0
mysql-common conda-forge/linux-64::mysql-common-8.0.30-haf5c9bc_0
mysql-libs conda-forge/linux-64::mysql-libs-8.0.30-h28c427c_0
nspr conda-forge/linux-64::nspr-4.32-h9c3ff4c_1
nss conda-forge/linux-64::nss-3.78-h2350873_0
openjpeg conda-forge/linux-64::openjpeg-2.5.0-h7d73246_1
packaging conda-forge/noarch::packaging-21.3-pyhd8ed1ab_0
pcre conda-forge/linux-64::pcre-8.45-h9c3ff4c_0
pillow conda-forge/linux-64::pillow-9.2.0-py39hd5dbb17_2
ply conda-forge/noarch::ply-3.11-py_1
portaudio conda-forge/linux-64::portaudio-19.6.0-h57a0ea0_5
pthread-stubs conda-forge/linux-64::pthread-stubs-0.4-h36c2ea0_1001
pulseaudio conda-forge/linux-64::pulseaudio-14.0-h7f54b18_8
pyparsing conda-forge/noarch::pyparsing-3.0.9-pyhd8ed1ab_0
pyqt conda-forge/linux-64::pyqt-5.15.7-py39h18e9c17_0
pyqt5-sip conda-forge/linux-64::pyqt5-sip-12.11.0-py39h5a03fae_0
python-dateutil conda-forge/noarch::python-dateutil-2.8.2-pyhd8ed1ab_0
qt-main conda-forge/linux-64::qt-main-5.15.4-ha5833f6_2
sip conda-forge/linux-64::sip-6.6.2-py39h5a03fae_0
toml conda-forge/noarch::toml-0.10.2-pyhd8ed1ab_0
tornado conda-forge/linux-64::tornado-6.2-py39hb9d737c_0
unicodedata2 conda-forge/linux-64::unicodedata2-14.0.0-py39hb9d737c_1
xcb-util conda-forge/linux-64::xcb-util-0.4.0-h166bdaf_0
xcb-util-image conda-forge/linux-64::xcb-util-image-0.4.0-h166bdaf_0
xcb-util-keysyms conda-forge/linux-64::xcb-util-keysyms-0.4.0-h166bdaf_0
xcb-util-renderut~ conda-forge/linux-64::xcb-util-renderutil-0.3.9-h166bdaf_0
xcb-util-wm conda-forge/linux-64::xcb-util-wm-0.4.1-h166bdaf_0
xorg-libxau conda-forge/linux-64::xorg-libxau-1.0.9-h7f98852_0
xorg-libxdmcp conda-forge/linux-64::xorg-libxdmcp-1.1.3-h7f98852_0
zstd conda-forge/linux-64::zstd-1.5.2-h6239696_4
Preparing transaction: ...working... done
Verifying transaction: ...working... done
Executing transaction: ...working... done
Visualization Examples
Now we demonstrate four cases that tend to appear with binning analyses:
- The binning analysis converges to a plateau for a correlated data stream $R_X(\ell \rightarrow \infty) > 0$.
- The binning analysis reveals that the data stream is truly uncorrelated $R_X(\ell \rightarrow \infty) = 0$.
- The binning analysis has not converged because too few data were taken in the data stream to isolate the uncorrelated data.
- The binning analyis has not convered because all of the data in the data stream are more-or-less equally correlated.
The first two cases are ideal. They represent situations where the data stream has enough data to distinguish correlated blocks from one another. In these first two cases, the BinningAnalysisResult
will yield plateau_found == true
.
The second two cases are not so ideal. Either way they show that the correlations in the data stream are so strong that individual uncorrelated blocks can not be robustly created and more sampling is required. Therefore, their BinningAnalysisResult
yields plateau_found == false
.
The data shown are pre-simulated signals generated by the TelegraphNoise.jl
package (v0.1.0
). Random telegraph signals have an analytically-defined autocorrelation time $\tau_X$ related to their average dwell time $T_D$ by $\tau_X = T_D / 2$. Since the signals are random, they are saved previously, but I'll provide each chosen $T_D$ for reproducibility purposes.
Example 1: Clear $R_X$ plateau at a finite value
This first case shows the textbook (James Gubernatis, Naoki Kawashima, Philipp Werner (2016)) example of an $R_X$ plateau found in a binning analysis. The dwell time for this signal was chosen to be $T_D = 16$, meaning $\tau_X = 8$, and the signal generated was $2^{18} = 262144$ in length.
# Read in pre-simulated TelegraphNoise.jl data with a plateau
signal = zeros(Float64, Int(2^18))
read!( joinpath("assets", "telegraph_plateau.bin"), signal )
bacc = BinningAccumulator()
push!(bacc, signal)
plot_binning_analysis(bacc)
result = fit_RxValues(bacc)
result.plateau_found
# output
true
Importantly, note that the fitted inflection point is greater than zero, but less than the maximum trustworthy level. The variations in the calculated $R_X$ values for $\ell$ greater than the maximum trustworthy level are due to strong fluctuations because of small number statistics.
Example 2: Uncorrelated data ~ $R_X(\ell \rightarrow \infty) = 0$
The dwell time for this signal was chosen to be $T_D = 1/2$, meaning $\tau_X = 1/4$, and the signal generated was $2^{14} = 16384$ in length.
# Read in pre-simulated TelegraphNoise.jl data with a plateau
signal = zeros(Float64, Int(2^14))
read!( joinpath("assets", "telegraph_uncorrelated.bin"), signal )
bacc = BinningAccumulator()
push!(bacc, signal)
plot_binning_analysis(bacc)
result = fit_RxValues(bacc)
result.plateau_found
# output
true
Notice that the $R_X$ values decay for increasing $\ell$. This is because the binning analysis cannot detect any smaller blocks of uncorrelated data since the original data stream is indeed correlated.
Example 3: No plateau due to data insufficiency
For insufficiently long data streams, we do not expect a plateau, as shown in the following case:
# Read in pre-simulated TelegraphNoise.jl data without a plateau
signal = zeros(Float64, Int(2^10))
read!( joinpath("assets", "telegraph_no_plateau.bin"), signal )
bacc = BinningAccumulator()
push!(bacc, signal)
plot_binning_analysis(bacc)
result = fit_RxValues(bacc)
result.plateau_found
# output
false
Here, the dwell time was chosen to be $T_D = 256$, meaning $\tau_X = 128$, and the signal generated was $2^{10} = 1024$ in length. Notice, that the binning analysis began to pick up on a set of uncorrelated blocks, but there was not enough simulated data to reveal them before the maximum trustworthy level was reached. In a case like this, one would simply need to run their simulation about 64 times longer reveal a fitted plateau.
Example 4: No plateau due to totally-correlated data
In this final example, we demonstrate what happens for a data stream that has "frozen-in" correlations. By this, we mean one where the autocorrelation time $\tau_X$ far exceeds the data stream size. For this case, $T_D = 2^{16} = 65536$, so $\tau_X = 2^{15} = 32768$, while the signal length is only $2^{12} = 4096$.
# Read in pre-simulated TelegraphNoise.jl data with a plateau
signal = zeros(Float64, Int(2^12))
read!( joinpath("assets", "telegraph_totally_correlated.bin"), signal )
bacc = BinningAccumulator()
push!(bacc, signal)
plot_binning_analysis(bacc)
result = fit_RxValues(bacc)
result.plateau_found
# output
false
In this case, eventually the binning analysis returns $R_X = 0$, since the blocks start comparing literally identical elements. Notice that this case would return a plateau_found
if it were not for the check if any of the $R_X$ values were too small. (In principle this case also would result from a data stream with a defined periodicity, despite having correlations, but this is unavoidable.)
This is the most dangerous case to automate because the var
iance of such a data stream is naturally very small relative to the mean
. As is the case with all Monte Carlo simulations, or statistical data streams, one should be very wary when the error is a mathematical zero, or vanishingly small compared to the calculated mean
.