Quartile called Median on empty half-slices when the input held one value
and discarded both errors, so it returned {NaN, v, NaN} with a nil error.
The NaN spread silently through InterQuartileRange, Midhinge and Trimean.
A single observation is its own lower hinge, median and upper hinge, which
is what Percentile already returns for the same data. Return that instead,
and note why the remaining discarded Median errors cannot fire.
87 lines
2.0 KiB
Go
87 lines
2.0 KiB
Go
package stats
|
|
|
|
import "math"
|
|
|
|
// Quartiles holds the three quartile points
|
|
type Quartiles struct {
|
|
Q1 float64
|
|
Q2 float64
|
|
Q3 float64
|
|
}
|
|
|
|
// Quartile returns the three quartile points from a slice of data
|
|
//
|
|
// A single value is its own lower hinge, median and upper hinge, so all
|
|
// three quartiles are that value.
|
|
func Quartile(input Float64Data) (Quartiles, error) {
|
|
|
|
il := input.Len()
|
|
if il == 0 {
|
|
return Quartiles{}, EmptyInputErr
|
|
}
|
|
|
|
// One value is its own quartiles, which also matches what Percentile
|
|
// returns for that value at any percent. The general path below cannot
|
|
// produce it: the cutoffs would leave both halves empty and the two
|
|
// hinges would come back as NaN.
|
|
if il == 1 {
|
|
return Quartiles{input[0], input[0], input[0]}, nil
|
|
}
|
|
|
|
// Start by sorting a copy of the slice
|
|
copy := sortedCopy(input)
|
|
|
|
// Find the cutoff places depeding on if
|
|
// the input slice length is even or odd
|
|
var c1 int
|
|
var c2 int
|
|
if il%2 == 0 {
|
|
c1 = il / 2
|
|
c2 = il / 2
|
|
} else {
|
|
c1 = (il - 1) / 2
|
|
c2 = c1 + 1
|
|
}
|
|
|
|
// Find the Medians with the cutoff points. Both halves hold at least
|
|
// one value for every length above one, so Median cannot fail here.
|
|
Q1, _ := Median(copy[:c1])
|
|
Q2, _ := Median(copy)
|
|
Q3, _ := Median(copy[c2:])
|
|
|
|
return Quartiles{Q1, Q2, Q3}, nil
|
|
|
|
}
|
|
|
|
// InterQuartileRange finds the range between Q1 and Q3
|
|
func InterQuartileRange(input Float64Data) (float64, error) {
|
|
if input.Len() == 0 {
|
|
return math.NaN(), EmptyInputErr
|
|
}
|
|
qs, _ := Quartile(input)
|
|
iqr := qs.Q3 - qs.Q1
|
|
return iqr, nil
|
|
}
|
|
|
|
// Midhinge finds the average of the first and third quartiles
|
|
func Midhinge(input Float64Data) (float64, error) {
|
|
if input.Len() == 0 {
|
|
return math.NaN(), EmptyInputErr
|
|
}
|
|
qs, _ := Quartile(input)
|
|
mh := (qs.Q1 + qs.Q3) / 2
|
|
return mh, nil
|
|
}
|
|
|
|
// Trimean finds the average of the median and the midhinge
|
|
func Trimean(input Float64Data) (float64, error) {
|
|
if input.Len() == 0 {
|
|
return math.NaN(), EmptyInputErr
|
|
}
|
|
|
|
c := sortedCopy(input)
|
|
q, _ := Quartile(c)
|
|
|
|
return (q.Q1 + (q.Q2 * 2) + q.Q3) / 4, nil
|
|
}
|