blob: fc76584ffffe07f05c2325ab76964d4df985a243 [file]
// The quantile package implements Effective Computation of Biased Quantiles
// over Data Streams http://www.cs.rutgers.edu/~muthu/bquant.pdf
//
// This package is useful for calculating targeted quantiles for large datasets
// within low memory and cpu bounds. This means your trading a small amount of
// accuracy in rank selection, for efficiency.
//
// NOTE: Multiple streams can be merged before a Query, allowing clients to be distributed across threads.
package quantile
import (
"container/list"
"math"
)
type Interface interface {
// Query returns the calculated qth percentiles value. Calling Query
// with q not in the set quantiles given to New will have non-deterministic
// results.
Query(q float64) float64
// Insert inserts v into the list.
Insert(v float64)
// Merge merges samples into the list. This handy when
// merging multiple streams from seperate threads.
Merge(samples Samples)
// Samples returns a copy of the list of samples kept from the data
// stream.
Samples() Samples
// Count returns the total number of samples observed in the stream
// since initialization.
Count() int
// Init initializes or clears the list.
Init()
// Min returns the minimum value observed in the list.
Min() float64
// Max returns the maximum value observed in the list.
Max() float64
}
type stream struct {
e float64
q []float64
n float64
l *list.List
max float64
}
// New returns an initialized stream targeted at quantiles for error e. e is usually 0.01.
func New(e float64, quantiles ...float64) Interface {
x := &stream{e: e, q: quantiles, l: list.New()}
return &buffer{x, make(Samples, 0, 500)}
}
func (qt *stream) Init() {
qt.l.Init()
qt.n = 0
}
func (qt *stream) ƒ(r float64) float64 {
var m float64 = math.MaxFloat64
var f float64
for _, q := range qt.q {
if q*qt.n <= r {
f = (2 * qt.e * r) / q
} else {
f = (2 * qt.e * (qt.n - r)) / (1 - q)
}
m = math.Min(m, f)
}
return m
}
func (qt *stream) Insert(v float64) {
fn := qt.mergeFunc()
fn(v, 1)
}
func (qt *stream) Merge(samples Samples) {
fn := qt.mergeFunc()
for _, s := range samples {
fn(s.Value, s.Width)
}
}
func (qt *stream) mergeFunc() func(v, w float64) {
// NOTE: I used a goto over defer because it bought me a few extra
// nanoseconds. I know. I know.
var r float64
e := qt.l.Front()
return func(v, w float64) {
if v > qt.max {
qt.max = v
}
for ; e != nil; e = e.Next() {
c := e.Value.(*Sample)
if c.Value > v {
s := &Sample{v, w, math.Floor(qt.ƒ(r)) - 1}
qt.l.InsertBefore(s, e)
goto inserted
}
r += c.Width
}
qt.l.PushBack(&Sample{v, w, 0})
inserted:
qt.n += w
}
}
func (qt *stream) Count() int {
return int(qt.n)
}
func (qt *stream) Query(q float64) float64 {
e := qt.l.Front()
t := math.Ceil(q * qt.n)
t += math.Ceil(qt.ƒ(t) / 2)
p := e.Value.(*Sample)
e = e.Next()
r := float64(0)
for e != nil {
c := e.Value.(*Sample)
if r+c.Width+c.Delta > t {
return p.Value
}
r += p.Width
p = c
e = e.Next()
}
return p.Value
}
func (qt *stream) compress() {
if qt.l.Len() < 2 {
return
}
e := qt.l.Back()
x := e.Value.(*Sample)
r := qt.n - 1 - x.Width
e = e.Prev()
for e != nil {
c := e.Value.(*Sample)
if c.Width+x.Width+x.Delta <= qt.ƒ(r) {
x.Width += c.Width
o := e
e = e.Prev()
qt.l.Remove(o)
} else {
x = c
e = e.Prev()
}
r -= c.Width
}
}
func (qt *stream) Samples() Samples {
samples := make(Samples, 0, qt.l.Len())
for e := qt.l.Front(); e != nil; e = e.Next() {
samples = append(samples, *e.Value.(*Sample))
}
return samples
}
// Min returns the mininmul value observed in the stream.
func (qt *stream) Min() float64 {
if e := qt.l.Front(); e != nil {
return e.Value.(*Sample).Value
}
return math.NaN()
}
// Max returns the maximum value observed in the stream within the error epsilon.
func (qt *stream) Max() float64 {
if qt.l.Len() > 0 {
return qt.max
}
return math.NaN()
}