From 1a6516501508cb4e1f4e67dc75ce6cfa809f78be Mon Sep 17 00:00:00 2001 From: Marcus Kammer Date: Tue, 20 Aug 2024 18:12:03 +0200 Subject: [PATCH] Bulletproof statistical functions --- dev.metalisp.survey.asd | 2 +- src/stats.lisp | 42 +++++++++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/dev.metalisp.survey.asd b/dev.metalisp.survey.asd index 1f91eae..752f9d4 100644 --- a/dev.metalisp.survey.asd +++ b/dev.metalisp.survey.asd @@ -2,7 +2,7 @@ (defsystem "dev.metalisp.survey" :description "A simple survey" - :version "0.4.6" + :version "0.4.7" :author "Marcus Kammer " :source-control "git@git.sr.ht:~marcuskammer/dev.metalisp.survey" :licence "MIT" diff --git a/src/stats.lisp b/src/stats.lisp index 9b6b025..c24767f 100644 --- a/src/stats.lisp +++ b/src/stats.lisp @@ -10,6 +10,9 @@ (in-package #:ml-survey/stats) +(define-condition stats-error (error) + ((message :initarg :message :reader error-message))) + (defun preprocess-and-transpose (data) (apply #'mapcar #'list (mapcar #'cdr data))) @@ -21,19 +24,34 @@ (reduce #'max numbers))) (defun standard-deviation (numbers) - (let* ((avg (mean numbers)) - (variance (/ (reduce #'+ (mapcar (lambda (x) (expt (- x avg) 2)) numbers)) - (1- (length numbers))))) - (sqrt variance))) + (handler-case + (let ((len (length numbers))) + (if (< len 2) + (error 'stats-error :message "Need at least two numbers for standard deviation") + (let ((avg (mean numbers))) + (sqrt (/ (reduce #'+ (mapcar (lambda (x) (expt (- x avg) 2)) numbers)) + (1- len)))))) + (type-error () + (error 'stats-error :message "Invalid input for standard deviation calculation")))) (defun mean (numbers) - (/ (reduce #'+ numbers) (length numbers))) + (handler-case + (if (null numbers) + (error 'stats-error :message "Cannot calculate mean of an empty list") + (/ (reduce #'+ numbers) (length numbers))) + (division-by-zero () + (error 'stats-error :message "Division by zero in mean calculation")))) (defun median (numbers) - (let ((sorted (sort (copy-seq numbers) #'<)) - (count (length numbers))) - (if (oddp count) - (nth (floor count 2) sorted) - (/ (+ (nth (1- (/ count 2)) sorted) - (nth (/ count 2) sorted)) - 2.0)))) + (handler-case + (let ((sorted (sort (copy-seq numbers) #'<)) + (len (length numbers))) + (if (zerop len) + (error 'stats-error :message "Cannot calculate median of an empty list") + (if (oddp len) + (nth (floor len 2) sorted) + (/ (+ (nth (1- (/ len 2)) sorted) + (nth (/ len 2) sorted)) + 2)))) + (type-error () + (error 'stats-error :message "Invalid input for median calculation"))))