Skip to content

Commit 9c2773b

Browse files
authored
Merge pull request #229 from labmlai/smooth
exponential smoothing
2 parents 54ba292 + 3c12d68 commit 9c2773b

File tree

7 files changed

+281
-146
lines changed

7 files changed

+281
-146
lines changed

app/ui/src/analyses/experiments/chart_wrapper/card.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import {Indicator} from "../../../models/run"
33
import {
44
AnalysisPreferenceModel, ComparisonPreferenceModel,
55
} from "../../../models/preferences"
6-
import {getChartType, smoothAndTrimAllCharts} from "../../../components/charts/utils"
6+
import {getChartType} from "../../../components/charts/utils"
77
import {LineChart} from "../../../components/charts/lines/chart"
88
import {SparkLines} from "../../../components/charts/spark_lines/chart"
9+
import {TwoSidedExponentialAverage} from "../../../components/charts/smoothing/two_sided_exponential_average"
910

1011
interface CardWrapperOptions {
1112
width: number
@@ -76,7 +77,16 @@ export class CardWrapper {
7677
this.smoothValue = preferenceData.smooth_value
7778
this.trimSmoothEnds = preferenceData.trim_smooth_ends
7879

79-
smoothAndTrimAllCharts(this.series, this.baseSeries, this.smoothValue, this.stepRange, this.trimSmoothEnds)
80+
let [smoothedSeries, smoothedBaseSeries] = (new TwoSidedExponentialAverage({
81+
indicators: this.series.concat(this.baseSeries ?? []) ?? [],
82+
smoothValue: this.smoothValue,
83+
min: this.stepRange[0],
84+
max: this.stepRange[1],
85+
currentIndicatorLength: this.series.length
86+
})).smoothAndTrim()
87+
88+
this.series = smoothedSeries
89+
this.baseSeries = smoothedBaseSeries
8090
}
8191

8292
public render() {

app/ui/src/analyses/experiments/chart_wrapper/view.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import {DeleteButton, SaveButton, ToggleButton} from "../../../components/button
44
import {LineChart} from "../../../components/charts/lines/chart"
55
import {SparkLines} from "../../../components/charts/spark_lines/chart"
66
import {
7-
getChartType,
8-
smoothAndTrimAllCharts
7+
getChartType
98
} from "../../../components/charts/utils"
109
import {NumericRangeField} from "../../../components/input/numeric_range_field"
1110
import {Loader} from "../../../components/loader"
1211
import {Slider} from "../../../components/input/slider"
1312
import {UserMessages} from "../../../components/user_messages"
1413
import {NetworkError} from "../../../network"
14+
import {TwoSidedExponentialAverage} from "../../../components/charts/smoothing/two_sided_exponential_average"
1515

1616
interface ViewWrapperOpt {
1717
dataStore: MetricDataStore
@@ -221,10 +221,10 @@ export class ViewWrapper {
221221
parent: this.constructor.name
222222
})
223223
this.smoothSlider = new Slider({
224-
min: 1,
224+
min: 0,
225225
max: 100,
226226
value: this.dataStore.smoothValue,
227-
step: 0.1,
227+
step: 0.001,
228228
onChange: (value: number) => {
229229
let changeHandler = new ChangeHandlers.SmoothValueHandler(this, value)
230230
changeHandler.change()
@@ -333,8 +333,16 @@ export class ViewWrapper {
333333
}
334334

335335
private smoothSeries() {
336-
smoothAndTrimAllCharts(this.dataStore.series, this.dataStore.baseSeries,
337-
this.dataStore.smoothValue, this.dataStore.stepRange, this.dataStore.trimSmoothEnds)
336+
let [series, baseSeries] = (new TwoSidedExponentialAverage({
337+
indicators: this.dataStore.series.concat(this.dataStore.baseSeries ?? []) ?? [],
338+
smoothValue: this.dataStore.smoothValue,
339+
min: this.dataStore.stepRange[0],
340+
max: this.dataStore.stepRange[1],
341+
currentIndicatorLength: this.dataStore.series.length
342+
})).smoothAndTrim()
343+
344+
this.dataStore.series = series
345+
this.dataStore.baseSeries = baseSeries
338346
}
339347

340348
private renderTopButtons() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {SeriesSmoothing} from "./smoothing_base";
2+
import {PointValue} from "../../../models/run";
3+
4+
export class ExponentialMovingAverage extends SeriesSmoothing {
5+
protected smooth(): void {
6+
let smoothingFactor = 1 - this.smoothValue / 100
7+
8+
for (let i = 0; i < this.indicators.length; i++) {
9+
let ind = this.indicators[i]
10+
11+
if (ind.series.length == 0) {
12+
continue
13+
}
14+
15+
let result: PointValue[] = []
16+
17+
let lastSmoothed = ind.series[0].value
18+
for (let j = 0; j < ind.series.length; j++) {
19+
let smoothed = lastSmoothed * (1 - smoothingFactor) + ind.series[j].value * smoothingFactor
20+
result.push({step: ind.series[j].step, value: ind.series[j].value, smoothed: smoothed,
21+
lastStep: ind.series[j].lastStep})
22+
lastSmoothed = smoothed
23+
}
24+
25+
ind.series = result
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {SeriesSmoothing, SeriesSmoothingOptions} from "./smoothing_base"
2+
import {Indicator, PointValue} from "../../../models/run"
3+
import {mapRange} from "../utils";
4+
5+
export class MovingAverage extends SeriesSmoothing {
6+
private readonly trimSmoothEnds: boolean
7+
private readonly smoothWindow: number[]
8+
9+
constructor(opt: SeriesSmoothingOptions, trimSmoothEnds: boolean ) {
10+
super(opt)
11+
this.trimSmoothEnds = trimSmoothEnds
12+
this.smoothWindow = this.getSmoothWindow(opt.indicators, opt.smoothValue)
13+
}
14+
15+
protected smooth(): void {
16+
for (let i = 0; i < this.indicators.length; i++) {
17+
let ind = this.indicators[i]
18+
let windowSize = this.smoothWindow[i]
19+
20+
let result: PointValue[] = []
21+
windowSize = ~~windowSize
22+
let extraWindow = windowSize / 2
23+
extraWindow = ~~extraWindow
24+
25+
let count = 0
26+
let total = 0
27+
28+
for (let i = 0; i < ind.series.length + extraWindow; i++) {
29+
let j = i - extraWindow
30+
if (i < ind.series.length) {
31+
total += ind.series[i].value
32+
count++
33+
}
34+
if (j - extraWindow - 1 >= 0) {
35+
total -= ind.series[j - extraWindow - 1].value
36+
count--
37+
}
38+
if (j>=0) {
39+
result.push({step: ind.series[j].step, value: ind.series[j].value, smoothed: total / count,
40+
lastStep: ind.series[j].lastStep})
41+
}
42+
}
43+
ind.series = result
44+
}
45+
}
46+
47+
private getSmoothWindow(indicators: Indicator[], smoothValue: number): number[] {
48+
let maxRange: number = Number.MIN_SAFE_INTEGER
49+
for (let ind of indicators) {
50+
if (ind.series.length > 1 && !ind.is_summary) {
51+
maxRange = Math.max(maxRange, ind.series[ind.series.length - 1].step - ind.series[0].step)
52+
}
53+
}
54+
if (maxRange == Number.MIN_SAFE_INTEGER) { // all single points. -> can't smooth
55+
let stepRange = []
56+
for (let _ of indicators) {
57+
stepRange.push(1)
58+
}
59+
return stepRange
60+
}
61+
62+
let smoothRange = mapRange(smoothValue, 1, 100, 1, 2*maxRange)
63+
64+
let stepRange = []
65+
66+
for (let ind of indicators) {
67+
if (ind.series.length >= 2 && !ind.is_summary) {
68+
let stepGap = ind.series[1].step - ind.series[0].step
69+
let numSteps = Math.max(1, Math.ceil(smoothRange / stepGap))
70+
stepRange.push(numSteps)
71+
} else { // can't smooth - just a single point
72+
stepRange.push(1)
73+
}
74+
}
75+
76+
return stepRange
77+
}
78+
79+
override trim(): void {
80+
this.indicators.forEach((ind, i) => {
81+
let localSmoothWindow = Math.floor(this.smoothWindow[i] / 2) // remove half from each end
82+
83+
if (ind.series.length <= 1) {
84+
localSmoothWindow = 0
85+
} else if (this.smoothWindow[i] >= ind.series.length) {
86+
localSmoothWindow = Math.floor(ind.series.length/2)
87+
}
88+
89+
let localMin = this.min
90+
let localMax = this.max
91+
92+
if (localMin == -1) {
93+
localMin = ind.series[0].step
94+
}
95+
if (this.trimSmoothEnds) {
96+
localMin = Math.max(localMin, ind.series[localSmoothWindow].step)
97+
}
98+
99+
if (localMax == -1) {
100+
localMax = ind.series[ind.series.length - 1].step
101+
}
102+
if (this.trimSmoothEnds) {
103+
localMax = Math.min(localMax, ind.series[ind.series.length - 1 - localSmoothWindow +
104+
(ind.series.length%2 == 0 && localSmoothWindow != 0 ? 1 : 0)].step) // get the mid value for even length series
105+
}
106+
107+
localMin = Math.floor(localMin) - 0.5
108+
localMax = Math.ceil(localMax) + 0.5
109+
110+
let minIndex = ind.series.length - 1
111+
let maxIndex = 0
112+
113+
for (let i = 0; i < ind.series.length; i++) {
114+
let p = ind.series[i]
115+
if (p.step >= localMin && p.step <= localMax) {
116+
minIndex = Math.min(i, minIndex)
117+
maxIndex = Math.max(i, maxIndex)
118+
}
119+
}
120+
121+
ind.lowTrimIndex = minIndex
122+
ind.highTrimIndex = maxIndex
123+
})
124+
}
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {Indicator} from "../../../models/run";
2+
3+
export interface SeriesSmoothingOptions {
4+
indicators: Indicator[]
5+
smoothValue: number
6+
min: number
7+
max: number
8+
currentIndicatorLength: number
9+
}
10+
11+
export abstract class SeriesSmoothing {
12+
protected indicators: Indicator[]
13+
protected readonly smoothValue: number
14+
protected min: number
15+
protected max: number
16+
protected currentIndicatorLength: number
17+
18+
constructor(opt: SeriesSmoothingOptions) {
19+
this.indicators = opt.indicators
20+
this.smoothValue = opt.smoothValue
21+
this.min = opt.min
22+
this.max = opt.max
23+
this.currentIndicatorLength = opt.currentIndicatorLength
24+
}
25+
26+
public smoothAndTrim(): Indicator[][] {
27+
this.smooth()
28+
this.trim()
29+
30+
return [this.indicators.slice(0, this.currentIndicatorLength),
31+
this.indicators.slice(this.currentIndicatorLength)]
32+
}
33+
34+
protected abstract smooth(): void
35+
36+
protected trim(): void {
37+
this.indicators.forEach((ind, i) => {
38+
if (ind.series.length == 0) {
39+
return
40+
}
41+
42+
let localMin = this.min == -1 ? ind.series[0].step : this.min
43+
let localMax = this.max == -1 ? ind.series[ind.series.length - 1].step : this.max
44+
45+
let minIndex = ind.series.length - 1
46+
let maxIndex = 0
47+
48+
for (let i = 0; i < ind.series.length; i++) {
49+
let p = ind.series[i]
50+
if (p.step >= localMin && p.step <= localMax) {
51+
minIndex = Math.min(i, minIndex)
52+
maxIndex = Math.max(i, maxIndex)
53+
}
54+
}
55+
56+
ind.lowTrimIndex = minIndex
57+
ind.highTrimIndex = maxIndex
58+
})
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {SeriesSmoothing} from "./smoothing_base"
2+
import {PointValue} from "../../../models/run"
3+
4+
export class TwoSidedExponentialAverage extends SeriesSmoothing {
5+
protected smooth(): void {
6+
let smoothingFactor = 1 - this.smoothValue / 100
7+
8+
for (let i = 0; i < this.indicators.length; i++) {
9+
let ind = this.indicators[i]
10+
11+
if (ind.series.length < 2) {
12+
continue
13+
}
14+
15+
let result: PointValue[] = []
16+
let forward_pass: number[] = []
17+
let lastSmoothed = ind.series[0].value
18+
for (let j = 0; j < ind.series.length; j++) {
19+
let smoothed = lastSmoothed * (1 - smoothingFactor) + ind.series[j].value * smoothingFactor
20+
forward_pass.push(smoothed)
21+
lastSmoothed = smoothed
22+
}
23+
24+
let backward_pass: number[] = []
25+
lastSmoothed = ind.series[ind.series.length - 1].value
26+
for (let j = ind.series.length - 1; j >= 0; j--) {
27+
let smoothed = lastSmoothed * (1 - smoothingFactor) + ind.series[j].value * smoothingFactor
28+
backward_pass.push(smoothed)
29+
lastSmoothed = smoothed
30+
}
31+
backward_pass = backward_pass.reverse()
32+
33+
for (let j = 0; j < ind.series.length; j++) {
34+
let smoothed = (forward_pass[j] + backward_pass[j]) / 2
35+
result.push({step: ind.series[j].step, value: ind.series[j].value, smoothed: smoothed,
36+
lastStep: ind.series[j].lastStep})
37+
}
38+
39+
ind.series = result
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)