Repository navigation
Fix precision loss in variance, standard deviation and related statistics (HF-491) - #1784
Merged
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hyperformula-docs | 91456eb | Commit Preview URL Branch Preview URL |
Oct 08 2026, 07:32 AM |
marcin-kordas-hoc
added a commit
that referenced
this pull request
Sep 30, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Performance comparison of head (91456eb) vs base (3a9c34b) |
The variance functions computed sum(x^2) - sum(x)^2 / n in one pass. When the mean is large relative to the spread, the two terms are nearly equal and cancel, so the result was 0, #NUM!, or a negative variance. MomentsAggregate now keeps the sums of deviations and of squared deviations from a shift (the first value added) in double-double arithmetic (new src/interpreter/doubleDouble.ts) and rebases them when two aggregates compose, so the range cache keeps working. The sum of squared deviations is evaluated and divided in double-double and rounded once. Affects VAR.S, VAR.P, STDEV.S, STDEV.P, VARA, VARPA, STDEVA, STDEVPA, their legacy names, and SUBTOTAL modes 7, 8, 10, 11, 107, 108, 110 and 111. A variance that is finite in 3.3.0 stays finite for values up to about 1e300. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
marcin-kordas-hoc
force-pushed
the
fix/variance-precision
branch
from
October 7, 2026 09:52
c325212 to
7a88902
Compare
sequba
force-pushed
the
fix/variance-precision
branch
from
October 7, 2026 17:22
6f0a708 to
7a88902
Compare
…in double-double (HF-491) (#1798) Stacked on #1784 (base `fix/variance-precision`): it uses `doubleDouble.ts` from there. Retarget to `develop` once #1784 is merged. ### Context `DEVSQ`, `COVARIANCE.P`, `COVARIANCE.S`, `SLOPE`, `STEYX` and the database functions `DSTDEV`, `DSTDEVP`, `DVAR`, `DVARP` already subtract the mean before squaring, so they are not affected by the cancellation fixed in #1784. They still round the mean to a double before subtracting it, and `STEYX` subtracts two nearly equal sums. When the values use all 15 significant digits and their mean is large relative to their spread, this costs most of the digits: | Formula (three values near 1e11, `y = {1,2,3}`) | Before | Exact (stored doubles) | After | |---|---|---|---| | `=SLOPE({1,2,3}, {100000000000.003,100000000000.004,100000000000.005})` | `1000.37` | 1000.5301841348769 | 1000.5301841348769 | | `=STEYX(` same `)` | `0.0187` | 0.006232737795987631 | 0.006232737795987631 | | `=DEVSQ({100000000000.003,100000000000.004,100000000000.005})` | `1.99815e-06` | 1.997842142979304e-06 | 1.997842142979304e-06 | ### The change New `src/interpreter/deviationSums.ts` computes the mean, the deviations and their sums of squares and products in double-double (`doubleDouble.ts` from #1784, plus a double-double division and a rounding helper) and rounds once at the end. `DEVSQ`, `COVARIANCE.P/S`, `SLOPE` and `STEYX` call it instead of the jStat helpers, and `DSTDEV`, `DSTDEVP`, `DVAR` and `DVARP` use it instead of their own plain-double loops. A division by zero or any other non-finite result is returned as before. ### Accuracy The reference is the exact result for the stored doubles (BigInt rational arithmetic, cross-checked with Python's `statistics` and mpmath). Minimum digits of agreement over 23 datasets, including the 9 NIST StRD univariate sets: | Function | Before | After | |---|---|---| | `DEVSQ` | 3.4 | **15** | | `COVARIANCE.S` | 14.3 | **15** | | `SLOPE` | 3.4 | **15** | | `STEYX` | 0 | **15** | | `DSTDEV` | 4.1 | **15** | On ordinary data the old code was already at 11.5–15 digits; the loss shows on 15-digit inputs and on near-perfect fits. A plain-double version of the same code reproduces the "before" numbers, so the comparison is meaningful. ### Cost Medians of 5 alternating process pairs, each the median of 7 runs, building an engine and evaluating the formula (so the figures include parsing and loading the data): `DEVSQ` over 100k cells 1.08x, `SLOPE` over 100k pairs 1.12x, `STEYX` 1.14x, `COVARIANCE.S` 1.05x, 2000 `DEVSQ` formulas over 100 cells each 1.25x, `DSTDEV` over 20k rows 0.97x. On running ranges, `=DEVSQ(A$1:An)` over 4,000 rows takes 1.4x as long as before (1.8 s to 2.5 s, medians over 6 processes on a quiet machine), because every formula recomputes the whole range and the work per value is larger. `STDEV.S` on running ranges is unaffected by the first pull request (+4%, inside the noise). ### Behavior changes - `SLOPE` and `STEYX` now return `#NUM!` when all the x values are equal. Before they returned an arbitrary number (`SLOPE` 0, `STEYX` 1.414 for y = 1, 2, 3). - `STEYX` returns 0 instead of `#NUM!` for points that lie exactly on a line, where the residual rounds to a tiny negative number. - Results that were finite in 3.3.0 stay finite for values up to about 1e300. Both behaviors are covered by tests and mentioned in the changelog entry. ### How did you test your changes? - 30 new tests in the tests repo, added to the spec of each function. Expected values are exact for the stored doubles and compared as a ratio within 1e-14 with `smartRounding` off. They cover a case near 1e7 and one near 1e11 per function, covariance with both series near 1e11 (where the old mean rounding showed in the 4th digit), values near 1e300 and 1e150 that stay finite, `STEYX` on points that lie exactly on a line, and `SLOPE`/`STEYX` with all x values equal. Against develop 12 fail, against the earlier head of this PR 7 fail (the values that overflowed), and with this change all pass. - Full suite: 6291 passed, 0 failed (3 skipped, as on develop). - Lint: no new warnings (104 in the changed files against 106 on develop); the new files have none. ### Not in this PR `CORREL`, `PEARSON`, `RSQ`, `F.TEST`, `T.TEST`, `Z.TEST`, `SKEW` and `SKEW.P` still use the jStat helpers. `INTERCEPT` does not exist yet (HF-413). ### Types of changes - [ ] Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore) - [ ] New feature or improvement (a non-breaking change that adds functionality) - [x] Bug fix (a non-breaking change that fixes an issue) - [ ] Additional language file, or a change to an existing language file (translations) - [ ] Change to the documentation ### Related issues: 1. Stacked on #1784 2. Tests: handsontable/hyperformula-tests#74 ### Checklist: - [x] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/docs/guide/contributing.html) and I confirm that my code follows the code style of this project. - [x] I have signed the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). - [ ] My change is compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard. - [ ] My change is compatible with Microsoft Excel. - [ ] My change is compatible with Google Sheets. - [x] I described my changes in the [CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md) file. - [ ] My changes require a documentation update. - [ ] My changes require a migration guide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes numeric results for affected formulas (intentional bug fix) and touches shared double-double math used by multiple statistical and database functions; edge-case error behavior for SLOPE/STEYX also changes. > > **Overview** > Fixes **numerical precision** for deviation-based statistics when inputs have a **large mean and tiny spread** (e.g. values near `1e11` differing only in the last digits). > > Adds **`deviationSums.ts`** to compute means, squared deviations, and covariance-style products in **double-double** (rounding once at the end), plus **`divideDoubleDoubles`** in `doubleDouble.ts`. **`DEVSQ`**, **`COVARIANCE.P` / `.S`** (and aliases), **`SLOPE`**, **`STEYX`**, and database **`DSTDEV`**, **`DSTDEVP`**, **`DVAR`**, **`DVARP`** now use this path instead of jStat / plain-double loops. > > **`STEYX`** no longer yields **`#NUM!`** for a perfect linear fit; **`SLOPE`** and **`STEYX`** return **`#NUM!`** when all **x** values are equal. **CHANGELOG** updated. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 54904ea. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bb43a62. Configure here.
Values of about 1e153 and above made VAR, STDEV, COVARIANCE, SLOPE, STEYX and the D-variance functions return #NUM! where develop returned a finite result. - twoProduct: scale the larger factor by 2^-53 when a factor exceeds 2^996 or the product exceeds 2^1023, so the Veltkamp split cannot overflow; the error term is now exact for every finite product above 2^-969. - twoSum: take the error from the larger-magnitude addend (Fast2Sum), which cannot overflow while the sum is finite. - divideDoubleDouble, divideDoubleDoubles: scale dividends above 2^1000 down by 2^64 so the quotient times the divisor cannot round to infinity. - MomentsAggregate: evaluate S1^2 / n as S1 * (S1 / n), and keep the plain sum of squares so the variance falls back to the previous one-pass form when the shifted sums overflow. - deviationSums mean(): sum scaled values when the running total overflows. - SLOPE, STEYX: return #NUM! when the sum of squared x deviations overflows, instead of a slope of 0. Also: - AVERAGE, AVERAGEA and SUBTOTAL 1/101 reduce a plain sum and count under their own cache keys instead of MomentsAggregate. - Shared variance/covariance and subtractDoubleDouble helpers; SLOPE and STEYX compute each array's deviations once. - Correct the accuracy claims in the JSDoc and the CHANGELOG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om SLOPE and STEYX for equal x values (HF-491) STEYX computed the explained sum of squares as slope * Sxy. When the slope overflowed, the residual became -Infinity and was clamped to 0. The explained sum is now computed as Sxy^2 / Sxx in that case, and a non-finite residual returns #NUM! instead of 0. When all the x values are equal, SLOPE and STEYX return #DIV/0!, as in Excel and Google Sheets, instead of #NUM!. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…GE in one pass (HF-491) MomentsAggregate stores the deviations from the shift multiplied by 2^-exponent, so the shifted sums cannot overflow. The exponent is raised when the scaled sum of squares exceeds 2^960, and two aggregates are composed at the larger exponent. The variance is rounded once and scaled back. It is #NUM! only when it exceeds the largest double, and it no longer depends on the order of the arguments. The plain sum and sum of squares and the one-pass fallback are removed. AVERAGE, AVERAGEA and SUBTOTAL 1/101 reduce a single AverageResult (sum and count), moved out of ConditionalAggregationPlugin into its own module, so each argument is evaluated once. Before, the sum and the count were reduced in two passes, which evaluated non-range arguments twice: nested AVERAGE calls took exponential time, and a volatile argument could give a sum and a count from different evaluations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sequba
reviewed
Oct 7, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rough doAverageA (HF-491) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…deviation sums by powers of two (HF-491) DVAR, DVARP, DSTDEV and DSTDEVP now use the same MomentsAggregate as VAR and STDEV, so they return the same results, including when the sum of squared deviations exceeds the largest double. MomentsAggregate moves to its own module, takes the standard deviation at the scale of its sums, and lowers its exponent for tiny differences, so STDEV no longer returns 0 for spreads below about 1e-154. COVARIANCE, SLOPE, STEYX and DEVSQ keep two passes over their arrays. An array whose largest magnitude is below 2^-400 is scaled up to it, so squared deviations cannot underflow and SLOPE and STEYX return #DIV/0! only when all the x values are equal. An array above 2^400 is scaled down only when the unscaled sums overflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocs of the variance changes (HF-491) SLOPE and STEYX share the equal-x check in one helper that returns #DIV/0! with ErrorMessage.EqualXValues. AverageResult gets JSDoc, the JSDoc of MomentsAggregate.of no longer claims the same results as a cached or split range, and the changelog lists VAR and the SUBTOTAL modes among the functions fixed for very large and very small values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es (HF-491)
STEYX computed the residual sum of squares as Syy - Sxy^2 / Sxx, which cancels
when the points lie almost on a line: for y = {1e8, 2e8, 300000000.00000024}
and x = {1, 2, 3} it was 0.8% off. The residuals are now computed one by one in
double-double and centered on their mean, which removes the shift that the
rounding errors of the x and y means add to every residual.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…angelog (HF-491) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sequba
approved these changes
Oct 8, 2026
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #1784 +/- ##
===========================================
+ Coverage 97.26% 97.28% +0.02%
===========================================
Files 203 207 +4
Lines 16022 16218 +196
Branches 3555 3588 +33
===========================================
+ Hits 15583 15777 +194
- Misses 431 433 +2
Partials 8 8
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Context
The variance functions compute
sum(x^2) - sum(x)^2 / nin one pass. When the mean is large relative to the spread (typical of measurement data), the two terms are nearly equal and cancel, so every significant digit is lost:=STDEV.S({10000000.000,10000000.001,10000000.002})#NUM!=VAR.S(same)-0.03125(negative, no error)=STDEV.S({100000000000.003,100000000000.004,100000000000.005})0smartRounding: falseandprecisionEpsilon: 0do not change the old results.DEVSQ,COVARIANCE.P,COVARIANCE.S,SLOPE,STEYXand the database functionsDSTDEV,DSTDEVP,DVAR,DVARPcompute the mean first and lose digits in the same situation when the values use all 15 significant digits. For example, for three values near100000000000,STEYXreturned0.0187instead of0.00623andSLOPEreturned1000.37instead of1000.53.Affected:
VAR.S,VAR.P,STDEV.S,STDEV.P,VARA,VARPA,STDEVA,STDEVPA, their legacy names (VAR,VARP,VARS,STDEV,STDEVP,STDEVS), andSUBTOTALmodes 7, 8, 10 and 11 (and 107, 108, 110, 111), all backed byMomentsAggregateDEVSQ,COVARIANCE.P,COVARIANCE.S(andCOVAR,COVARIANCEP,COVARIANCES),SLOPE,STEYX,DSTDEV,DSTDEVP,DVAR,DVARPThe change
Double-double helpers (
src/interpreter/doubleDouble.ts): TwoSum (in the Fast2Sum form, taking the error from the larger addend, so it cannot overflow while the sum is finite) and Dekker's TwoProduct with Veltkamp's split (no FMA needed), plus addition, subtraction, multiplication and division of double-doubles, and rounding to a double. Near the top of the double range they rescale by exact powers of two:twoProductscales the larger factor by 2^-53 when a factor exceeds 2^996 or the product exceeds 2^1023, so its error term is exact for every finite product above 2^-969, and the divisions scale dividends above 2^1000 by 2^-64.MomentsAggregate(src/interpreter/plugin/MomentsAggregate.ts) stays composable, so the per-range cache keeps working. Besides the count, it keeps the sums of deviationsS1and of squared deviationsS2from ashift(the first value added), stored as double-double, and rebases them when two aggregates compose. The deviations are stored multiplied by2^-exponent, an exact power of two kept in the aggregate, so the shifted sums cannot overflow: the exponent is 0 until the scaled sum of squares exceeds 2^960 (deviations of about 1e144 and above), and two aggregates compose at the larger exponent, raised further when the difference of their shifts needs it. Adding one value, the common case when a range is folded cell by cell, takes a fast path. The sum of squared deviationsS2 - S1^2 / nis evaluated in double-double asS2 - S1 * (S1 / n), so theS1^2term cannot overflow, the variance is rounded once, and it is then multiplied by2^(2 * exponent). The variance is#NUM!only when it exceeds the largest double, and it does not depend on the order of the arguments:=VAR.S(0, 1e154, 1e154)and=VAR.S(1e154, 1e154, 0)both return 3.33e307 (develop returns#NUM!for both), and=VAR.S(-1.2e154, 0, 1.2e154)returns 1.44e308 although the sum of squared deviations exceeds the largest double. The exponent can also go negative (down to -600) when tiny differences follow equal values, and the standard deviation is taken at the scale of the sums before scaling back, soSTDEV.*is finite when only the variance overflows and non-zero for spreads below about 1e-154 (=STDEV.S(1e-170, 2e-170)returns 7.07e-171).Deviation sums (
src/interpreter/deviationSums.ts):DEVSQ,COVARIANCE.*,SLOPEandSTEYXcompute the mean in double-double, then the deviations and the sums of their products in double-double, and round once. Each array is measured at its own power-of-two scale and the rounded result is scaled back: an array whose largest magnitude is below 2^-400 is scaled up to it (exact), so squared deviations cannot underflow and the sum of squared x deviations is 0 only when all the x values are equal; an array above 2^400 is scaled down to it only when the unscaled sums overflow, so on ordinary data the results are bit-identical to the unscaled computation.DEVSQis only scaled up, because when its unscaled sum overflows, its result does too.SLOPEandSTEYXshareregressionSumsand the check for equal x values.STEYXdoes not useSyy - Sxy^2 / Sxx, which cancels when the points lie almost on a line: it computes each residualdy - slope * dxin double-double, centers the residuals on their mean (which removes the shift that the rounding errors of the two means add to every residual), and sums their squares.SLOPEskips the residuals.D-variance functions:
DVAR,DVARP,DSTDEVandDSTDEVPfold their values throughMomentsAggregate, so they return the same results asVAR.S,VAR.P,STDEV.SandSTDEV.P, including when the sum of squared deviations overflows.Behaviour changes besides precision:
STEYXreturns the standard error instead of#NUM!or0for points that lie almost on a line (=STEYX({0.25,0.4,0.5499999999999999},{0.1,0.2,0.3})returns 2.83e-17, the exact value for the stored doubles, which are not evenly spaced, where develop returns#NUM!;=STEYX({100000000,200000000,300000000.00000024},{1,2,3})returns 9.733e-8 where develop returns0);SLOPEandSTEYXreturn#DIV/0!with the message "All x values are equal." (the newErrorMessage.EqualXValues) when all the x values are equal, as Excel and Google Sheets return#DIV/0!(develop returns#NUM!or an arbitrary number), and finite results where the sum of squared deviations of the x values overflows or underflows (=SLOPE({1,2},{1e-170,2e-170})returns 1e170; develop returns#NUM!).DVAR,DSTDEVandCOVARIANCEreturn finite results where only an intermediate sum overflows (=DSTDEVof-1.2e154, 0, 1.2e154returns 1.2e154, likeSTDEV.S).AVERAGE,AVERAGEAandSUBTOTAL1/101 no longer useMomentsAggregate: they reduce anAverageResult(a plain sum and a count, moved fromConditionalAggregationPluginto its own module) under their own cache keys (_AVERAGEand_AVERAGE_A), so they do not compute the double-double sums. Each argument is evaluated once, and the results are the same as before.Accuracy
The reference is the exact result for the stored doubles, reported as LRE (digits of agreement, McCullough 1998). Exact references: BigInt rational arithmetic, Python's
statistics(exact fractions) and mpmath at 60 digits. The data is 24 datasets, including all 9 NIST StRD univariate sets. Measured forVAR/STDEVat 7a88902 and not re-measured since; the variance algorithm is the same apart from theS1 * (S1 / n)evaluation and the power-of-two exponent, which is exact scaling and is 0 for all 24 datasets.sd()Double-double arithmetic does not guarantee correct rounding, so the JSDoc states the accuracy as about one unit in the last place. Split arguments,
=STDEV.S(A1:A50, A51:A100)in either order, hold the same accuracy. When a range reuses the cached aggregate of a smaller range or the values come in several arguments, partial aggregates are composed in a different order than the left-to-right fold of the D-functions, so the results can differ from them in the last bits.Compared with Excel: near 1e7 the results are equal, but near 1e11 Excel's two-pass result differs from the exact value in the 4th significant digit (0.000999538 vs 0.000999460), and for 50 identical values whose mean is not exactly representable Excel returns 7.5e-8 instead of 0. This PR returns the exact values.
Alternatives measured
MomentsAggregatefunctions: mean first, then deviations, which needs every value again. It cannot use the range cache, and a running=STDEV.S(A$1:An)over 5,000 rows became 10x slower to build and 38x slower to recalculate after one edit. The functions that already take whole arrays (DEVSQ,COVARIANCE.*,SLOPE,STEYX, D-functions) use two passes, in double-double.How did you test your changes?
statistics, compared as a ratio within 1e-14 withsmartRoundingoff. They cover every affected function, the legacySTDEVname,SUBTOTAL7 and 11, two ranges as separate arguments, range + scalar, a range extending a cached smaller range, two cached ranges kept at different scales (tiny spreads of different sizes, and tiny values with huge ones) combined in either order, recalculation after an edit, the NIST NumAcc4 dataset, identical values (also near 1e300), variances near 1e300,STEYXon points exactly and almost on a line,SLOPE/STEYXwith equal x values (including the error message), and overflow: a squared deviation close to the largest double in either order, shifted sums that overflow, two cached ranges whose combination overflows, a running total that overflows, a value equal to the largest double, products of deviations near the largest double that cancel, the sum of squared x deviations overflowing or underflowing inSLOPE/STEYX, the D-functions andCOVARIANCEmatchingVAR/STDEVwhen the sum of squared deviations overflows, tiny spreads inSTDEV.SandDSTDEV, products of large and tiny values that cancel inCOVARIANCEandSLOPE, an overflowing slope inSTEYX, the same variance for the values in any order and for a range split at any point when the squared deviations from the first value overflow, a finite variance whose sum of squared deviations overflows,#NUM!when the variance exceeds the largest double,AVERAGEandVAR.SandAVERAGEAandVARAover the same range, andAVERAGE,AVERAGEAandSUBTOTAL1 evaluating each argument once.STEYXon points almost on a line was checked against exact rational results: 4,000 datasets (n from 3 to 12, intercepts, slopes and x values over 30 to 40 orders of magnitude, residual sums of squares down to 1e-37 of the sum of squared y deviations), largest relative error 1.8e-15, plus 60k datasets of the generalCOVARIANCE/SLOPE/STEYXfuzz with noSTEYXresult off by more than 1e-12.twoSumandtwoProduct(boundary grids around 2^996, 2^1023, the largest double, subnormals, signed zeros, infinities and NaN, plus random bit patterns) with no inexact result where an exact one is representable, and about 5M cases each for the two divisions.VAR.*,STDEV.*,VARA,VARPA,STDEVA,STDEVPAandSUBTOTAL7, 8, 10 and 11. Where the exact variance is a normal double, the largest error is 1 ulp (3 results at 2 ulp just above 2^-1022; largest relative error 3.75e-16), all forms agree within 2 ulp, and#NUM!is returned exactly when the exact variance rounds to infinity.STDEV.Sover 100k cells 14.5 → 18.5 ms (1.28x),AVERAGEplusVAR.Sover the same 20k-cell range 5.6 → 7.2 ms (1.29x), recalculation of a runningSTDEV.S(A$1:An)over 5k rows after one edit 12.9 → 13.9 ms (1.08x), building a runningSTDEV.S(A$1:An)orAVERAGE(A$1:An)over 5k rows 1.00x and 0.99x. Data that needs the exponent (values near 1e150) adds about 6% to the 100kSTDEV.S.COVARIANCE,SLOPEandSTEYXwas checked against exact BigInt results and against the previous head (92ebfbd): about 18k random datasets with 23 formulas each, 18k more from an independent generator, an 80k-datasetMomentsAggregatecomposition fuzz and 19,664 equal-x cases. No result differs from 92ebfbd on ordinary data, the D-functions equalVAR/STDEVin every regime, and equal x values always give#DIV/0!. On 100k ordinary values, every affected function is within about ±3% of 92ebfbd, andDVAR/DSTDEVare about 5% faster.STDEV.S10.49 → 11.82 MB,AVERAGE10.48 → 10.96 MB. A cachedMomentsAggregateis larger than on develop (two double-doubles, a shift and an exponent); a cachedAverageResultis smaller than develop's aggregate.Not in this PR
VAR.*results below 2^-1022 (spreads below about 1e-154) are subnormal numbers with few significant bits; they are rounded at the scale of the sums and then scaled back, so they can be 1 subnormal ulp off.CORREL,PEARSON,RSQ,Z.TEST,T.TEST,F.TEST,SKEWandSKEW.Pstill compute their sums of deviations in plain doubles (jStat), so they keep the precision loss on data with a large mean.COVARIANCEandSLOPEscale large arrays down, which rounds away values about 2^1400 times smaller than the largest one. On extremely ill-conditioned data (values near the largest double that cancel, or data spanning about 600 orders of magnitude) they can then return a finite but inaccurate result where develop returned#NUM!; no double-double method can resolve these.STEYXrounds twice (division, then square root), so it can be about 1 ulp off.divideDoubleDoubleswith a dividend below about 2^-950 can be a few ulps less accurate than plain division, and sums or quotients within half an ulp of the largest double return infinity instead of the largest double.AVERAGE:=AVERAGE(1e16, 1, -1e16)returns0, as on develop and in Excel.INTERCEPTis not implemented in HyperFormula.Types of changes
Related issues:
DEVSQ,COVARIANCE,SLOPE,STEYXand the D-variance functions)Checklist:
🤖 Generated with Claude Code
Note
Medium Risk
Touches core numeric paths for many spreadsheet functions; behavior changes (errors, edge magnitudes) are intentional but affect formula results users may rely on.
Overview
Fixes catastrophic cancellation in variance, standard deviation, and related statistics when the mean is large relative to the spread (e.g.
STDEV.Son values near1e7with tiny differences).Introduces double-double arithmetic (
doubleDouble.ts) and two aggregation layers:MomentsAggregatefor composableVAR/STDEV/SUBTOTAL/databaseDVAR/DSTDEV(shifted sums with power-of-two scaling), anddeviationSums.tsforDEVSQ,COVARIANCE,SLOPE, andSTEYX.STEYXnow sums squared residuals per point (with centered residuals) instead of a formula that cancels on nearly collinear data.SLOPEandSTEYXreturn#DIV/0!with "All x values are equal." when all x values match.AVERAGE/AVERAGEAuse a lightweightAverageResult(sum + count) instead of the variance aggregate. Release script step 3 readsCURRENT_VERSIONafter the step label.Reviewed by Cursor Bugbot for commit 91456eb. Bugbot is set up for automated code reviews on this repo. Configure here.