-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathp2quantile.go
More file actions
147 lines (136 loc) · 4.1 KB
/
Copy pathp2quantile.go
File metadata and controls
147 lines (136 loc) · 4.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package streamstats
// P2Quantile is an O(1) time and space data structure
// for estimating the p-quantile of a series of N data points based on the
// "The P2 Algorithm for Dynamic Computing Calculation of Quantiles and
// Histograms Without Storing Observations" by RAJ JAIN and IIMRICH CHLAMTAC
// Communications of the ACM Volume 28 Issue 10, Oct. 1985 Pages 1076-1085
type P2Quantile struct {
p float64 // the p-quantile to be tracked
n [5]uint64 // the actual counts for each marker
np [5]float64 // the target counts for each marker
dnp [5]float64 // the updates to the target counts for each additional measurement
q [5]float64 // the value of each marker, i.e. the estimated quantile
}
// NewP2Quantile intializes the data structure to track the p-quantile
func NewP2Quantile(p float64) P2Quantile {
return P2Quantile{
p: p,
n: [5]uint64{1, 2, 3, 4, 0},
np: [5]float64{1, 1 + 2*p, 1 + 4*p, 3 + 2*p, 5},
dnp: [5]float64{0, p / 2, p, (1 + p) / 2, 1},
}
}
// Add updates the data structure with a given x value
func (p *P2Quantile) Add(x float64) {
if p.n[4] < 5 {
// Initialization:
i := p.n[4] // the current count
p.q[i] = x // add the new element on the end
// insertion sort the elements
for i > 0 && p.q[i-1] > p.q[i] {
t := p.q[i-1]
p.q[i-1] = p.q[i]
p.q[i] = t
i--
}
p.n[4]++
} else {
// find which bin the new element lies in
var k uint64
switch {
case x < p.q[0]:
p.q[0] = x // new minimum
k = 0
case x < p.q[1]:
k = 0
case x < p.q[2]:
k = 1
case x < p.q[3]:
k = 2
case x < p.q[4]:
k = 3
default:
p.q[4] = x // new maximum
k = 3
}
// update the actual counts for the markers
for i := k + 1; i < 5; i++ {
p.n[i]++
}
// update the goal counts for the markers
for i := 0; i < 5; i++ {
p.np[i] += p.dnp[i]
}
// adjust heights of internal markers if necessary
for i := 1; i < 4; i++ {
d := p.np[i] - float64(p.n[i]) // the difference from the target
if (d >= 1.0 && p.n[i]+1 < p.n[i+1]) || (d <= -1.0 && p.n[i-1]+1 < p.n[i]) {
// delta is always snapped to +/- 1
if d >= 1.0 {
d = 1.0
} else {
d = -1.0
}
// try using the piecewise polynomial degree 2 formula
fNm := float64(p.n[i-1])
fN := float64(p.n[i])
fNp := float64(p.n[i+1])
qp := p.q[i] + d*((fN-fNm+d)*(p.q[i+1]-p.q[i])/(fNp-fN)+(fNp-fN-d)*(p.q[i]-p.q[i-1])/(fN-fNm))/(fNp-fNm)
if p.q[i-1] < qp && qp < p.q[i+1] {
p.q[i] = qp
} else { // use linear formula if degree 2 formula would result in out of order markers
ip := i + int(d)
p.q[i] += d * (p.q[ip] - p.q[i]) / (float64(p.n[ip]) - fN)
}
if d > 0 { // increment the counter for the bin after adjustments were made
p.n[i]++
} else {
p.n[i]--
}
}
}
}
}
// P returns the quantile being tracked
func (p *P2Quantile) P() float64 {
return p.p
}
// N returns the number of observations seen so far
func (p *P2Quantile) N() uint64 {
return p.n[4]
}
// Quantile returns the estimated value for the p-quantile
func (p *P2Quantile) Quantile() float64 {
if p.n[4] < 5 && 0 < p.n[4] {
if p.n[4]%2 == 0 {
return (p.q[p.n[4]/2-1] + p.q[p.n[4]/2]) / 2 // average of values around median for even N
}
return p.q[p.n[4]/2] // the median value seen in the first 5 for odd N
}
return p.q[2] // the estimate of the median
}
// UpperQuantile returns the estimate for the upper quantile, (1+p/2)
func (p *P2Quantile) UpperQuantile() float64 {
if p.n[4] < 5 && 0 < p.n[4] {
return (p.Quantile() + p.Max()) / 2 // average the data if we don't have enough points yet
}
return p.q[3]
}
// LowerQuantile returns the estimate for the lower quantile, p/2
func (p *P2Quantile) LowerQuantile() float64 {
if p.n[4] < 5 && 0 < p.n[4] {
return (p.Min() + p.Quantile()) / 2 // average the data if we don't have enough points yet
}
return p.q[1]
}
// Max returns the exact maximum value seen so far
func (p *P2Quantile) Max() float64 {
if p.n[4] < 5 && 0 < p.n[4] {
return p.q[p.n[4]-1] // the highest for small counts
}
return p.q[4]
}
// Min returns the exact minimum value seen so far
func (p *P2Quantile) Min() float64 {
return p.q[0]
}