Skip to content
Jennifer Programming Language

stats - statistics: descriptive, distributions, and inference

The stats library is the whole statistical toolkit: descriptive statistics over a list of int or list of float, the probability distributions those build on, and the classical inference layer (regression, confidence intervals, hypothesis tests). It is pure-value and dependency-free (Go stdlib only, the distribution CDFs reusing math's special functions), so both binaries build it. There is deliberately no separate prob library - distributions live with the descriptive stats and inference they are used with, as in scipy.stats or R's base dnorm / pnorm / qnorm.

jennifer
use stats;
def xs as list of int init [2, 4, 4, 4, 5, 5, 7, 9];
stats.mean($xs);              # 5.0
stats.median($xs);            # 4.5
stats.stddev($xs);            # 2.0
stats.percentile($xs, 90);    # 7.6

Functions

CallReturnsNotes
stats.mean(xs)floatArithmetic mean. Empty list errors.
stats.median(xs)floatMiddle value (mean of the two middles for an even count). Empty errors.
stats.mode(xs)element kindMost frequent element (first-reached wins a tie). Empty errors.
stats.variance(xs)floatPopulation variance σ² (Σ(x - μ)² / n). Empty errors.
stats.stddev(xs)floatPopulation standard deviation σ = √variance. Empty errors.
stats.sampleVariance(xs)floatSample variance s² (÷ n-1, Bessel-corrected). Needs >= 2 elements.
stats.sampleStddev(xs)floatSample standard deviation s = √sampleVariance (the "empirical" SD). Needs >= 2.
stats.mad(xs)floatMedian absolute deviation - a robust (outlier-resistant) spread. Empty errors.
stats.skewness(xs)floatPopulation skewness (3rd standardized moment); 0 symmetric, > 0 right tail.
stats.kurtosis(xs)floatExcess kurtosis (4th moment - 3); 0 for a normal, > 0 heavy tails.
stats.percentile(xs, p)floatThe p-th percentile (p in [0, 100]), linear interpolation. See below.
stats.quartiles(xs)list of float[Q1, Q2, Q3] = [p25, p50, p75]. Empty errors.
stats.iqr(xs)floatInterquartile range = p75 - p25. Empty errors.
stats.min(xs)element kindSmallest element. Empty errors.
stats.max(xs)element kindLargest element. Empty errors.
stats.range(xs)element kindThe spread max - min (int for an all-int list, overflow-checked). Empty errors.
stats.sum(xs)int / floatSum: int for an all-int list (overflow errors), float otherwise. sum([]) is 0.
stats.geometricMean(xs)floatGeometric mean (n-th root of the product). Every element must be > 0.
stats.harmonicMean(xs)floatHarmonic mean (n / sum of reciprocals). Every element must be > 0.
stats.weightedMean(xs, ws)floatWeighted mean sum(x*w) / sum(w). Equal-length; weights must not sum to 0.
stats.zscore(xs)list of floatEach element standardized to (x - mean) / stddev (population). Constant list errors.
stats.modes(xs)listEvery modal value (all tied for the top frequency), in first-seen order; keeps kind.
stats.correlation(xs, ys)floatPearson correlation of two equal-length lists (>= 2 elements). See below.
stats.covariance(xs, ys)floatPopulation covariance σ_xy (÷ n), the units-aware companion to correlation.
stats.sampleCovariance(xs, ys)floatSample covariance s_xy (÷ n-1), the companion to sampleVariance. Needs >= 2.
stats.describe(xs)stats.SummaryOne-call {count, min, q1, median, mean, q3, max, stddev}. See below.

Each function accepts a list of int or a list of float (an int element promotes to float for the real-valued reductions). A non-numeric element, or a non-list argument, is a positioned error.

Kind-preserving vs. real-valued

The reductions that are inherently real-valued - mean, median, variance, stddev, percentile, correlation - always return float. The selections that pick an element - min, max, mode - and sum preserve the input kind: over a list of int they return an int, over a list of float a float.

jennifer
stats.min([2, 4, 9]);        # 2   (int)
stats.min([2.0, 4.0]);       # 2.0 (float)
stats.sum([1, 2, 3]);        # 6   (int, overflow-checked)
stats.sum([1.5, 2.5]);       # 4.0 (float)

Population vs. sample (σ vs s)

variance / stddev are population statistics (σ², σ - divide by n) - the list is treated as the whole data set, matching NumPy's default. sampleVariance / sampleStddev are the sample (s², s - Bessel-corrected, ÷ (n-1)) forms for inferential use, and need at least two elements:

text
population   σ² = Σ(x - μ)² / n           σ = √σ²
sample       s² = Σ(x - x̄)² / (n - 1)     s = √s²

where μ (population) / x̄ (sample) is the mean. The sample form s is what some sources call the empirical standard deviation; Jennifer names the two forms explicitly (stddev vs sampleStddev) rather than by that ambiguous label. covariance follows the population convention (σ_xy, ÷ n) to match variance; sampleCovariance is the sample form (s_xy, ÷ (n-1)). The normalized correlation divides out the standard deviations, so its n factor cancels and the population/sample distinction does not matter there. The companion linalg library does linear algebra over the same value types; further ML primitives atop the two are on the horizon.

Percentile

stats.percentile(xs, p) sorts the data and interpolates linearly between the two nearest ranks (NumPy's default method): p = 0 is the minimum, p = 100 the maximum, and p = 50 equals median. p may be an int or float; a value outside [0, 100], or an empty list, is an error.

Correlation

stats.correlation(xs, ys) is the Pearson correlation coefficient in [-1, 1]: 1.0 for a perfect positive linear relationship, -1.0 for a perfect negative one, 0.0 for none. The two lists must be the same length with at least two elements, and neither may have zero variance (a constant list) - all of which are catchable errors. stats.covariance(xs, ys) is the raw, units-aware version (the numerator, ÷ n).

Quartiles, IQR, and z-scores

stats.quartiles(xs) returns [Q1, Q2, Q3] ([p25, p50, p75]) as a list of float; stats.iqr(xs) is p75 - p25, useful for spread and outlier detection. stats.range(xs) is the full spread max - min (kind-preserving). stats.zscore(xs) standardizes every element to (x - mean) / stddev (population), returning a list of float - a constant list has no z-score and errors.

Geometric and harmonic means

stats.geometricMean(xs) (the n-th root of the product, computed via logs so a long list does not overflow) suits growth rates and ratios; stats.harmonicMean(xs) (n / sum of reciprocals) suits averaging rates. Both require every element to be strictly positive. stats.weightedMean(xs, weights) averages xs weighted by weights (same length; the weights must not sum to zero).

Shape

stats.skewness(xs) is the population skewness (the standardized third moment): 0 for a symmetric distribution, positive when the right tail is longer. stats.kurtosis(xs) is the excess kurtosis (the standardized fourth moment minus 3), so a normal distribution is 0 and heavier-than-normal tails are positive. Both need at least two elements and a non-constant list. stats.mad(xs) (median absolute deviation) and stats.modes(xs) (every modal value, versus mode's single one) round out the robust / multimodal summaries.

describe

stats.describe(xs) computes a whole summary in one pass and returns a stats.Summary struct - count (int) plus min, q1, median, mean, q3, max, and stddev (population, all float):

jennifer
def s as stats.Summary init stats.describe($data);
io.printf("median %v, IQR %v\n", $s.median, $s.q3 - $s.q1);

Distributions

The probability distributions inference is built on, with flat R-style names. Every function returns a float. The continuous CDFs reduce to math's regularized incomplete gamma / beta; the quantiles (inverse CDFs) invert them numerically. Sampling draws on math's shared random source (so math.randSeed makes it reproducible).

CallMeaning
stats.normalPdf(x, mean, sd) / normalCdf / normalQuantile(p, mean, sd) / normalSample(mean, sd)Normal (Gaussian) N(μ, σ²). sd > 0.
stats.tPdf(x, df) / tCdf(x, df) / tQuantile(p, df)Student's t. df > 0.
stats.chiSquareCdf(x, df) / chiSquareQuantile(p, df)Chi-square (χ²). x >= 0, df > 0.
stats.fCdf(x, df1, df2) / fQuantile(p, df1, df2)F distribution. x >= 0, dfs > 0.
stats.binomialPmf(k, n, p) / binomialCdf(k, n, p)Binomial. k, n ints; 0 <= p <= 1.
stats.poissonPmf(k, lambda) / poissonCdf(k, lambda)Poisson. k int; lambda > 0.

A quantile probability must be in the open interval (0, 1) (the boundaries map to infinity); an out-of-domain parameter is a catchable error, never a NaN.

jennifer
use stats;
stats.normalCdf(1.96, 0, 1);        # 0.975...
stats.normalQuantile(0.975, 0, 1);  # 1.959...
stats.tQuantile(0.975, 10);         # 2.228... (t critical value)
stats.binomialPmf(5, 10, 0.5);      # 0.2461

Inference

Regression, confidence intervals, and hypothesis tests. Results come back as small structs whose fields are:

jennifer
def struct Regression { n as int, slope as float, intercept as float, r as float, r2 as float, stdErr as float, pValue as float };
def struct Interval { lower as float, upper as float };
def struct Test { statistic as float, df1 as float, df2 as float, pValue as float };

Regression is a simple-OLS fit: slope / intercept, the correlation r and its square r2, the slope's standard error stdErr, its two-sided pValue, and the point count n. Interval is a [lower, upper] confidence interval. Test carries the test statistic, its degrees of freedom df1 / df2 (df2 is 0 for a single-df test like the t or chi-square; both are set for an F / ANOVA), and the pValue.

CallReturnsMeaning
stats.linearRegression(xs, ys)RegressionSimple OLS: slope, intercept, r, r2, stdErr (of the slope), pValue, n.
stats.multipleRegression(X, ys)list of floatCoefficients [intercept, b1, b2, ...] for the design rows X (normal-equations solve).
stats.confidenceInterval(data, level)Intervalt-based CI for the mean at confidence level in (0, 1).
stats.proportionCi(successes, n, level, method)IntervalBinomial-proportion interval; method is "wald", "wilson", or "clopper-pearson" (exact).
stats.tTest(data, mu)TestOne-sample t-test of the mean against mu.
stats.tTest2(a, b)TestTwo-sample Welch t-test (unequal variances).
stats.chiSquareTest(observed, expected)TestChi-square goodness-of-fit; expected counts must be positive.
stats.fTest(a, b)TestTwo-sided F-test for equal variances.
stats.anova(groups)TestOne-way ANOVA over a list of groups.
stats.histogram(data, binEdges)list of intBin counts (Excel FREQUENCY); k+1 ascending edges give k bins, the last closed on the right.

A degenerate input (zero variance, a singular design, a non-positive expected count, a negative observed count, or magnitudes that overflow the computation) is a catchable error.

A few boundary and numerical notes:

  • Proportion intervals at the extremes. For proportionCi at successes == 0 or successes == n, the "wald" and "wilson" formulas collapse to a degenerate point interval ([0, 0] / [1, 1]) - the well-known boundary defect of those methods. Use "clopper-pearson" (the exact interval) when a boundary count matters; it yields a proper one-sided bound there.
  • Regression conditioning. linearRegression / multipleRegression solve the normal equations by Gaussian elimination (the same approach as linalg.solve). That is fast and exact for well-conditioned data, but numerically weak for near-collinear predictors or extreme scales; the coefficients can lose precision before the design is flagged singular. Center / scale the predictors when they span very different magnitudes.
  • Cost. multipleRegression is O(n*k²) to form the system and O(k³) to solve it, for k predictors - fine for the modest tabular data a tree-walker handles, not a many-thousand-column design.
jennifer
use stats;
use io;

def r as stats.Regression init stats.linearRegression([1, 2, 3, 4, 5], [2.1, 3.9, 6.2, 7.8, 10.1]);
io.printf("y = %.2fx + %.2f (r2 %.3f, p %.4f)\n", $r.slope, $r.intercept, $r.r2, $r.pValue);

def ci as stats.Interval init stats.proportionCi(8, 10, 0.95, "clopper-pearson");
io.printf("95%% CI: [%.3f, %.3f]\n", $ci.lower, $ci.upper);

def t as stats.Test init stats.tTest([5.1, 4.9, 5.2, 4.8, 5.0], 4.5);
io.printf("t = %.3f, p = %.4f\n", $t.statistic, $t.pValue);

Strictness

Following math's stance, a non-finite or mathematically undefined result is a catchable error rather than a NaN or Inf: an empty list (for every reduction except sum), a percentile p outside [0, 100], mismatched or too-short correlation inputs, a zero-variance correlation, and an int sum that overflows int64 all raise a positioned error you can try / catch. The same applies when an input's magnitudes overflow the computation - a list of float with values near the float64 ceiling (~1.8e308) can push an intermediate sum or power to +/-Inf (and thence NaN); every real-valued reduction rejects a non-finite result instead of returning it, so a NaN never escapes into the type system.