diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index a7a75a8..e0b9e24 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -2183,6 +2183,9 @@ func Quartile(input Float64Data) (Quartiles, error) ``` 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. + diff --git a/quartile.go b/quartile.go index 40bbf6e..bd10033 100644 --- a/quartile.go +++ b/quartile.go @@ -10,6 +10,9 @@ type Quartiles struct { } // 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() @@ -17,6 +20,14 @@ func Quartile(input Float64Data) (Quartiles, error) { 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) @@ -32,7 +43,8 @@ func Quartile(input Float64Data) (Quartiles, error) { c2 = c1 + 1 } - // Find the Medians with the cutoff points + // 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:]) diff --git a/quartile_test.go b/quartile_test.go index 93d524e..28fb353 100644 --- a/quartile_test.go +++ b/quartile_test.go @@ -41,6 +41,47 @@ func TestQuartile(t *testing.T) { } } +func TestQuartile_SingleValue(t *testing.T) { + quartiles, err := stats.Quartile([]float64{43}) + if err != nil { + t.Errorf("Should not have returned an error; got %v", err) + } + if quartiles.Q1 != 43 { + t.Errorf("Q1 %v != 43", quartiles.Q1) + } + if quartiles.Q2 != 43 { + t.Errorf("Q2 %v != 43", quartiles.Q2) + } + if quartiles.Q3 != 43 { + t.Errorf("Q3 %v != 43", quartiles.Q3) + } + + // The quartiles of one value agree with Percentile on the same value. + for _, percent := range []float64{25, 50, 75} { + value, err := stats.Percentile([]float64{43}, percent) + if err != nil || value != 43 { + t.Errorf("Percentile %v on one value: got %v (err %v)", percent, value, err) + } + } +} + +func TestQuartile_SingleValueDependents(t *testing.T) { + iqr, err := stats.InterQuartileRange([]float64{43}) + if err != nil || iqr != 0 { + t.Errorf("IQR of one value: got %v (err %v), want 0", iqr, err) + } + + mh, err := stats.Midhinge([]float64{43}) + if err != nil || mh != 43 { + t.Errorf("Midhinge of one value: got %v (err %v), want 43", mh, err) + } + + tr, err := stats.Trimean([]float64{43}) + if err != nil || tr != 43 { + t.Errorf("Trimean of one value: got %v (err %v), want 43", tr, err) + } +} + func TestInterQuartileRange(t *testing.T) { s1 := []float64{102, 104, 105, 107, 108, 109, 110, 112, 115, 116, 118} iqr, _ := stats.InterQuartileRange(s1)