Skip to content

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 →