Files
enkly-ai/pkg/mathkit/statistics.go
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

225 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mathkit
import (
"fmt"
"math"
)
// Mean returns the arithmetic mean of data.
func Mean(data []float64) (float64, error) {
if len(data) == 0 {
return 0, fmt.Errorf("mean: empty data set")
}
var sum float64
for _, v := range data {
sum += v
}
return sum / float64(len(data)), nil
}
// Variance returns the variance of data. population=true divides by
// n (the population variance); population=false (the default for ML
// work, where `data` is almost always a sample) divides by n-1
// (Bessel's correction, the unbiased sample-variance estimator).
func Variance(data []float64, population bool) (float64, error) {
n := len(data)
if n == 0 {
return 0, fmt.Errorf("variance: empty data set")
}
if !population && n < 2 {
return 0, fmt.Errorf("variance: sample variance needs at least 2 points (use population variance for a single point)")
}
mean, _ := Mean(data)
var ss float64
for _, v := range data {
d := v - mean
ss += d * d
}
denom := float64(n)
if !population {
denom = float64(n - 1)
}
return ss / denom, nil
}
// StdDev returns the standard deviation (sqrt of Variance).
func StdDev(data []float64, population bool) (float64, error) {
v, err := Variance(data, population)
if err != nil {
return 0, err
}
return math.Sqrt(v), nil
}
// Median returns the median of data (average of the two middle
// values for an even-length slice). Does not mutate the input.
func Median(data []float64) (float64, error) {
if len(data) == 0 {
return 0, fmt.Errorf("median: empty data set")
}
sorted := append([]float64(nil), data...)
insertionSort(sorted)
n := len(sorted)
if n%2 == 1 {
return sorted[n/2], nil
}
return (sorted[n/2-1] + sorted[n/2]) / 2, nil
}
// insertionSort is O(n^2) but this package only ever sorts small
// stats samples, where it beats sort.Float64s' overhead and keeps
// the package allocation-free apart from the clone itself.
func insertionSort(a []float64) {
for i := 1; i < len(a); i++ {
key := a[i]
j := i - 1
for j >= 0 && a[j] > key {
a[j+1] = a[j]
j--
}
a[j+1] = key
}
}
// Covariance returns the sample (or population) covariance of two
// equal-length data sets — the building block of both Correlation and
// a PCA covariance matrix.
func Covariance(x, y []float64, population bool) (float64, error) {
if len(x) != len(y) {
return 0, fmt.Errorf("covariance: data sets have different lengths (%d vs %d)", len(x), len(y))
}
n := len(x)
if n == 0 {
return 0, fmt.Errorf("covariance: empty data set")
}
if !population && n < 2 {
return 0, fmt.Errorf("covariance: sample covariance needs at least 2 points")
}
mx, _ := Mean(x)
my, _ := Mean(y)
var sum float64
for i := 0; i < n; i++ {
sum += (x[i] - mx) * (y[i] - my)
}
denom := float64(n)
if !population {
denom = float64(n - 1)
}
return sum / denom, nil
}
// Correlation returns the Pearson correlation coefficient of x and y,
// in [-1, 1].
func Correlation(x, y []float64) (float64, error) {
cov, err := Covariance(x, y, true)
if err != nil {
return 0, fmt.Errorf("correlation: %w", err)
}
sx, err := StdDev(x, true)
if err != nil {
return 0, fmt.Errorf("correlation: %w", err)
}
sy, err := StdDev(y, true)
if err != nil {
return 0, fmt.Errorf("correlation: %w", err)
}
if sx == 0 || sy == 0 {
return 0, fmt.Errorf("correlation: a constant data set has undefined correlation")
}
return cov / (sx * sy), nil
}
// ZScoreNormalize returns (x - mean) / stddev for each point — the
// standardization every ML feature-scaling pipeline applies before
// training a distance- or gradient-sensitive model.
func ZScoreNormalize(data []float64) ([]float64, error) {
mean, err := Mean(data)
if err != nil {
return nil, fmt.Errorf("z_score_normalize: %w", err)
}
std, err := StdDev(data, true)
if err != nil {
return nil, fmt.Errorf("z_score_normalize: %w", err)
}
if std == 0 {
return nil, fmt.Errorf("z_score_normalize: a constant data set has zero standard deviation")
}
out := make([]float64, len(data))
for i, v := range data {
out[i] = (v - mean) / std
}
return out, nil
}
// MinMaxNormalize rescales data to [0, 1].
func MinMaxNormalize(data []float64) ([]float64, error) {
if len(data) == 0 {
return nil, fmt.Errorf("min_max_normalize: empty data set")
}
lo, hi := data[0], data[0]
for _, v := range data {
if v < lo {
lo = v
}
if v > hi {
hi = v
}
}
if hi == lo {
return nil, fmt.Errorf("min_max_normalize: a constant data set has zero range")
}
out := make([]float64, len(data))
for i, v := range data {
out[i] = (v - lo) / (hi - lo)
}
return out, nil
}
// NormalPDF is the probability density function of N(mean, stdDev^2)
// at x.
func NormalPDF(x, mean, stdDev float64) (float64, error) {
if stdDev <= 0 {
return 0, fmt.Errorf("normal_pdf: std_dev must be positive")
}
z := (x - mean) / stdDev
return math.Exp(-0.5*z*z) / (stdDev * math.Sqrt(2*math.Pi)), nil
}
// NormalCDF is the cumulative distribution function of N(mean,
// stdDev^2) at x, computed via math.Erf (the standard closed form:
// Φ(x) = (1 + erf((x-μ)/(σ√2))) / 2).
func NormalCDF(x, mean, stdDev float64) (float64, error) {
if stdDev <= 0 {
return 0, fmt.Errorf("normal_cdf: std_dev must be positive")
}
return 0.5 * (1 + math.Erf((x-mean)/(stdDev*math.Sqrt2))), nil
}
// BinomialPMF returns P(X = k) for X ~ Binomial(n, p).
func BinomialPMF(k, n int, p float64) (float64, error) {
if n < 0 || k < 0 || k > n {
return 0, fmt.Errorf("binomial_pmf: require 0 <= k <= n, got k=%d n=%d", k, n)
}
if p < 0 || p > 1 {
return 0, fmt.Errorf("binomial_pmf: p must be in [0, 1], got %g", p)
}
return binomialCoeff(n, k) * math.Pow(p, float64(k)) * math.Pow(1-p, float64(n-k)), nil
}
// binomialCoeff computes C(n, k) via the multiplicative formula,
// which stays in float64 range far longer than computing n!/(k!(n-k)!)
// from raw factorials would.
func binomialCoeff(n, k int) float64 {
if k < 0 || k > n {
return 0
}
if k > n-k {
k = n - k
}
result := 1.0
for i := 0; i < k; i++ {
result *= float64(n-i) / float64(i+1)
}
return result
}