stats.standard-deviation
Population or sample standard deviation of a list of numbers, two-pass, rounded to stated decimals.
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 18 tests, run in TypeScript, Python and Rust.
What it does
How spread out a set of numbers is. The caller says which one they mean, because the two differ and both are called "the standard deviation":
- `population` divides the sum of squared deviations by n. Use it when the values are the whole population (every invoice this month). Excel `STDEV.P`, NumPy `std()`. - `sample` divides by n - 1 (Bessel's correction). Use it when the values are a sample standing in for a larger population. Excel `STDEV.S`, R `sd()`, Python `statistics.stdev`. It needs at least two values.
For example
standard_deviation(2, 4, 4, 4, 5, 5, 7, 9, population, 6)→ 2 the textbook population example is exactly 2standard_deviation(2, 4, 4, 4, 5, 5, 7, 9, sample, 6)→ 2.138 the same data as a sample is sqrt(32/7)standard_deviation(0, 1, population, 0)→ 1 a population deviation of exactly 0.5 rounds away from zero to 1
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn standard_deviation(values: &[f64], kind: &str, decimals: i64) -> f64
| values | float[] | the data, at least one value (two for a sample) |
| kind | DeviationKind | population divides by n (Excel STDEV.P); sample divides by n - 1 (Excel STDEV.S) |
| decimals | int | 0 to 12; the result is rounded half away from zero to this many places |
| returns | float |
The type it declares, generated into your project
// DeviationKind is a string in Rust, one of: "population", "sample".
// Parameters take it as &str and results hold it as String.
Your code names it in one line, in the file that uses it
fune!(stats.standard-deviation@^1); // then call standard_deviation(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
const POW10: [f64; 13] = [
1.0, 10.0, 100.0, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12,
];
// Half away from zero on the binary64 value. A deviation is never negative,
// so there is no sign to restore.
fn round_to(x: f64, decimals: usize) -> f64 {
let scale = POW10[decimals];
let y = x * scale;
let mut r = y.floor();
if y - r >= 0.5 {
r += 1.0;
}
r / scale + 0.0
}
/// Population or sample standard deviation, two-pass.
///
/// The one-pass "mean of squares minus square of mean" shortcut cancels
/// catastrophically on large, close values; two passes do not.
///
/// # Panics
/// Panics on a non-finite value, an unknown kind, decimals outside 0..=12, an
/// empty list, or a sample of fewer than two values.
pub fn standard_deviation(values: &[f64], kind: &str, decimals: i64) -> f64 {
for v in values {
if !v.is_finite() {
panic!("values must be finite numbers, received {}", v);
}
}
if kind != "population" && kind != "sample" {
panic!("unknown standard deviation kind \"{}\"", kind);
}
if !(0..=12).contains(&decimals) {
panic!("decimals must be a whole number from 0 to 12, received {}", decimals);
}
let n = values.len();
if n == 0 {
panic!("values must not be empty");
}
if kind == "sample" && n < 2 {
panic!("sample standard deviation needs at least 2 values");
}
let mut sum = 0.0;
for v in values {
sum += *v;
}
let mean = sum / n as f64;
let mut squares = 0.0;
for v in values {
let d = *v - mean;
squares += d * d;
}
let divisor = if kind == "sample" { n - 1 } else { n } as f64;
round_to((squares / divisor).sqrt(), decimals as usize)
}
pub fn fune_vector(args: &[Value]) -> Value {
// Refuse what the typed signature cannot hold, with the wording TypeScript
// and Python use, rather than let the conversion below quietly change it.
for v in args[0].as_arr() {
if !matches!(v, Value::Int(_) | Value::Float(_)) {
panic!("values must be finite numbers, received {:?}", v);
}
}
let values: Vec<f64> = args[0].as_arr().iter().map(|v| v.as_f64()).collect();
Value::Float(standard_deviation(&values, args[1].as_str(), args[2].as_i64()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add stats.standard-deviation
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./stats.standard-deviation-1.0.0-rust.fune, or fetch it from a terminal with fune pull stats.standard-deviation@1.0.0:rust.
The whole function, every language, is one file too: stats.standard-deviation-1.0.0.fune, 12,588 bytes, sha256 aea4bddbb12330d26f7bdff755ca24697691ed9aece2b97892c034b13986048d. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before stats.standard-deviation
after — your function gets the result and the arguments, and returns the final result.
// fune: after stats.standard-deviation
replace — it requires no other capability, so there is no dependency to replace.
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show stats.standard-deviation --steps.
// fune: step stats.standard-deviation after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| the textbook population example is exactly 2 | 2, 4, 4, 4, 5, 5, 7, 9, population, 6 | → | 2 |
| the same data as a sample is sqrt(32/7) | 2, 4, 4, 4, 5, 5, 7, 9, sample, 6 | → | 2.138 |
| a population deviation of exactly 0.5 rounds away from zero to 1 | 0, 1, population, 0 | → | 1 |
| sample of 0 and 1 is sqrt(1/2) | 0, 1, sample, 4 | → | 0.707 |
| large close values do not cancel (population sqrt 22.5) | 1,000,000,004, 1,000,000,007, 1,000,000,013, 1,000,000,016, population, 6 | → | 4.743 |
| large close values do not cancel (sample sqrt 30) | 1,000,000,004, 1,000,000,007, 1,000,000,013, 1,000,000,016, sample, 6 | → | 5.477 |
| one value has no population spread | 42.5, population, 3 | → | 0 |
| a constant sample has no spread | 3, 3, 3, sample, 3 | → | 0 |
| negative values: population of -1 and 1 is 1 | -1, 1, population, 3 | → | 1 |
| negative values: sample of -1 and 1 is sqrt 2 | -1, 1, sample, 3 | → | 1.414 |
Show the other 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| decimal inputs: population of 0.1, 0.2, 0.3 is sqrt(2/300) | 0.1, 0.2, 0.3, population, 6 | → | 0.082 |
| twelve places of sqrt 1.25 | 1, 2, 3, 4, population, 12 | → | 1.118 |
| zero places rounds down below a half | 1, 2, 3, 4, population, 0 | → | 1 |
| an empty list is an error | , population, 2 | → | error: values must not be empty |
| a sample of one value is an error | 5, sample, 2 | → | error: sample standard deviation needs at least 2 values |
| an unknown kind is an error | 1, 2, unbiased, 2 | → | error: unknown standard deviation kind "unbiased" |
| negative decimals is an error | 1, 2, sample, -1 | → | error: decimals must be a whole number from 0 to 12 |
| a non-number value is an error | 1, x, sample, 2 | → | error: values must be finite numbers |
More from the author
**Algorithm.** Two passes: the mean first, then the sum of squared deviations from it. The one-pass textbook shortcut, mean of squares minus square of the mean, cancels catastrophically when the values are large and close together (four readings near one billion come out as garbage or even a negative variance); the vectors include that case.
**Precision.** The result is rounded to `decimals` places, half away from zero, applied to the binary64 value: y = x x 10^decimals, r = floor(y), plus one if y - r >= 0.5, divided back by 10^decimals. So a deviation of exactly 0.5 rounds to 1 at zero places, where Python's `round()` gives 0.
**Why the three languages agree to the bit.** Sums run left to right, and only IEEE-754 +, -, x, /, square root and floor are used. All of these are correctly rounded by the standard (square root included, unlike sin or exp, whose last bit varies between math libraries), so TypeScript, Python and Rust hold the same double before rounding and return the same rounded value.
Sources: NIST/SEMATECH e-Handbook of Statistical Methods, section 1.3.5.6 "Measures of Scale"; B. P. Welford, "Note on a Method for Calculating Corrected Sums of Squares and Products", Technometrics 4(3), 1962, on why the one-pass formula fails.
Files
| Path | Bytes |
|---|---|
| README.md | 1,816 |
| impl/python.py | 2,117 |
| impl/rust.rs | 2,347 |
| impl/typescript.ts | 1,792 |
| vectors.json | 2,266 |