Statistics Calculator — Go source
Compute descriptive statistics - count, sum, mean, median, mode, min/max, range, variance, standard deviation, and quartiles (Q1/Q3/IQR) - from any list of numbers. Tolerates mixed separators and flags unparseable tokens. Choose sample (n−1) or population (n) variance. Everything runs 100% client-side.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package statistics is the Go twin of CosmoDev's src/lib/statistics.ts (dual
// source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure + deterministic, never panics. The table-driven tests in
// statistics_test.go share vectors with src/lib/statistics.test.ts so the two
// implementations are held to the same contract.
//
// Behavior mirrors the TS lib: free-form number parsing splits on any run of
// whitespace and/or commas; descriptive stats use the R-7 (NumPy / Excel
// PERCENTILE) linear-interpolation quantile and the sample (n-1) variance
// estimator by default. Empty input returns count 0 with every numeric field
// NaN and no mode — never panics.
package statistics
import (
"math"
"regexp"
"sort"
"strconv"
"strings"
)
// ParseResult is the outcome of splitting a free-form number list into valid
// and invalid tokens. Mirrors ParseResult in src/lib/statistics.ts.
type ParseResult struct {
// Values are the finite numbers, in the order they appeared.
Values []float64
// Invalid are the tokens that could not be parsed as finite numbers, in order.
Invalid []string
}
// Stats holds descriptive statistics over a sample of numbers. Numeric fields
// are NaN when Count == 0; Mode is empty when there is no mode. Mirrors Stats
// in src/lib/statistics.ts.
type Stats struct {
Count int
Sum float64
Mean float64
Median float64
Mode []float64 // most frequent value(s), ascending; empty when uniform / no mode
Min float64
Max float64
Range float64
Variance float64
Stddev float64
Q1 float64
Q3 float64
IQR float64
}
// sepRe splits a free-form list on any run of whitespace and/or commas — the
// Go counterpart of /[\s,]+/ in the TS lib.
var sepRe = regexp.MustCompile(`[\s,]+`)
// ParseNumbers parses a free-form number list into finite values and
// unparseable tokens. Separators are any run of whitespace and/or commas.
// Tokens like "Infinity" and "NaN" parse via strconv.ParseFloat but are not
// finite, so they land in Invalid — mirroring Number.isFinite in the TS lib.
// Empty input yields no values and no invalid tokens. It is the Go twin of
// parseNumbers() in src/lib/statistics.ts.
func ParseNumbers(input string) ParseResult {
if strings.TrimSpace(input) == "" {
return ParseResult{}
}
var values []float64
var invalid []string
for _, tok := range sepRe.Split(input, -1) {
if tok == "" {
continue
}
n, err := strconv.ParseFloat(tok, 64)
// Number(tok) in JS yields Infinity/NaN for those literals; only finite
// results count as values, everything else (parse error or non-finite)
// is collected as an invalid token.
if err == nil && !math.IsNaN(n) && !math.IsInf(n, 0) {
values = append(values, n)
} else {
invalid = append(invalid, tok)
}
}
return ParseResult{Values: values, Invalid: invalid}
}
// quantile is the linear-interpolation quantile (R-7 / NumPy / Excel
// PERCENTILE convention). sorted must be ascending and non-empty; p in [0,1].
// Mirrors quantile() in the TS lib.
func quantile(sorted []float64, p float64) float64 {
n := len(sorted)
h := float64(n-1) * p
lower := math.Floor(h)
upper := math.Ceil(h)
if lower == upper {
return sorted[int(lower)]
}
return sorted[int(lower)] + (h-lower)*(sorted[int(upper)]-sorted[int(lower)])
}
// computeMode returns the most frequent value(s), ascending. It returns nil
// when there is no mode — i.e. when every value is distinct, or when all
// distinct values share the same frequency (a flat / uniform distribution with
// ≥2 distinct values). A single repeated value (e.g. [5,5,5]) does have a
// mode: [5]. Mirrors computeMode() in the TS lib.
func computeMode(values []float64) []float64 {
freq := map[float64]int{}
for _, v := range values {
freq[v]++
}
// A single distinct value is always the mode (covers [7] and [5,5,5]).
if len(freq) == 1 {
out := make([]float64, 0, 1)
for v := range freq {
out = append(out, v)
}
sort.Float64s(out)
return out
}
max := 0
for _, c := range freq {
if c > max {
max = c
}
}
var modes []float64
for v, c := range freq {
if c == max {
modes = append(modes, v)
}
}
// All distinct values share the max frequency → uniform → no mode.
if len(modes) == len(freq) {
return nil
}
sort.Float64s(modes)
return modes
}
// emptyStats returns the all-NaN Stats block for the empty-input case
// (count 0), mirroring EMPTY_STATS in the TS lib.
func emptyStats() Stats {
nan := math.NaN()
return Stats{
Count: 0,
Sum: nan,
Mean: nan,
Median: nan,
Mode: nil,
Min: nan,
Max: nan,
Range: nan,
Variance: nan,
Stddev: nan,
Q1: nan,
Q3: nan,
IQR: nan,
}
}
// summarize is the shared core of Summarize / SummarizePopulation. With
// sample=true variance/stddev use the sample estimator (n-1); with sample=false
// they use the population estimator (n). It mirrors summarize() in the TS lib.
func summarize(values []float64, sample bool) Stats {
n := len(values)
if n == 0 {
return emptyStats()
}
sorted := make([]float64, n)
copy(sorted, values)
sort.Float64s(sorted)
sum := 0.0
for _, x := range values {
sum += x
}
mean := sum / float64(n)
min := sorted[0]
max := sorted[n-1]
median := quantile(sorted, 0.5)
q1 := quantile(sorted, 0.25)
q3 := quantile(sorted, 0.75)
// Sum of squared deviations from the mean.
ss := 0.0
for _, x := range values {
d := x - mean
ss += d * d
}
var variance float64
if sample {
if n >= 2 {
variance = ss / float64(n-1)
} else {
variance = math.NaN()
}
} else {
variance = ss / float64(n)
}
stddev := math.NaN()
if !math.IsNaN(variance) && !math.IsInf(variance, 0) {
stddev = math.Sqrt(variance)
}
return Stats{
Count: n,
Sum: sum,
Mean: mean,
Median: median,
Mode: computeMode(values),
Min: min,
Max: max,
Range: max - min,
Variance: variance,
Stddev: stddev,
Q1: q1,
Q3: q3,
IQR: q3 - q1,
}
}
// Summarize computes descriptive statistics over values using the sample
// estimator (n-1) for variance/stddev — the default of summarize(values) in the
// TS lib. Empty input returns count 0 with every numeric field NaN and no mode.
func Summarize(values []float64) Stats {
return summarize(values, true)
}
// SummarizePopulation computes descriptive statistics using the population
// estimator (n) for variance/stddev — matching summarize(values, false) in the
// TS lib.
func SummarizePopulation(values []float64) Stats {
return summarize(values, false)
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →