From f8ffdaedc9a8fac631deafbc1d377430c980af94 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 13 Nov 2018 14:06:35 -0330 Subject: Modify results of API data to better fit gas chart: remove outliers, pad data --- ui/app/ducks/gas.duck.js | 93 ++++++++++++++++++++++++++++++++++--- ui/app/ducks/tests/gas-duck.test.js | 49 +++++++++---------- 2 files changed, 107 insertions(+), 35 deletions(-) (limited to 'ui/app/ducks') diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index ffd1e773f..20a125dbb 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -1,4 +1,4 @@ -import { clone, uniqBy } from 'ramda' +import { clone, uniqBy, flatten } from 'ramda' import BigNumber from 'bignumber.js' import { loadLocalStorageData, @@ -266,6 +266,56 @@ export function fetchBasicGasAndTimeEstimates () { } } +function extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) { + higherY = new BigNumber(higherY, 10) + lowerY = new BigNumber(lowerY, 10) + higherX = new BigNumber(higherX, 10) + lowerX = new BigNumber(lowerX, 10) + xForExtrapolation = new BigNumber(xForExtrapolation, 10) + const slope = (higherY.minus(lowerY)).div(higherX.minus(lowerX)) + const newTimeEstimate = slope.times(higherX.minus(xForExtrapolation)).minus(higherY).negated() + + return Number(newTimeEstimate.toPrecision(10)) +} + +function getRandomArbitrary (min, max) { + min = new BigNumber(min, 10) + max = new BigNumber(max, 10) + const random = new BigNumber(String(Math.random()), 10) + return new BigNumber(random.times(max.minus(min)).plus(min)).toPrecision(10) +} + +function calcMedian (list) { + const medianPos = (Math.floor(list.length / 2) + Math.ceil(list.length / 2)) / 2 + return medianPos === Math.floor(medianPos) + ? (list[medianPos - 1] + list[medianPos]) / 2 + : list[Math.floor(medianPos)] +} + +function quartiles (data) { + const lowerHalf = data.slice(0, Math.floor(data.length / 2)) + const upperHalf = data.slice(Math.floor(data.length / 2) + (data.length % 2 === 0 ? 0 : 1)) + const median = calcMedian(data) + const lowerQuartile = calcMedian(lowerHalf) + const upperQuartile = calcMedian(upperHalf) + return { + median, + lowerQuartile, + upperQuartile, + } +} + +function inliersByIQR (data, prop) { + const { lowerQuartile, upperQuartile } = quartiles(data.map(d => prop ? d[prop] : d)) + const IQR = upperQuartile - lowerQuartile + const lowerBound = lowerQuartile - 1.5 * IQR + const upperBound = upperQuartile + 1.5 * IQR + return data.filter(d => { + const value = prop ? d[prop] : d + return value >= lowerBound && value <= upperBound + }) +} + export function fetchGasEstimates (blockTime) { return (dispatch, getState) => { const { @@ -289,21 +339,50 @@ export function fetchGasEstimates (blockTime) { .then(r => { const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice })) const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes) - const timeMappedToSeconds = estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }) => { - const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).toString(10) + + const withSupplementalTimeEstimates = flatten(estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }, i, arr) => { + const next = arr[i + 1] + if (!next) { + return [{ expectedWait, gasprice }] + } else { + const supplementalPrice = getRandomArbitrary(gasprice, next.gasprice) + const supplementalTime = extrapolateY({ + higherY: next.expectedWait, + lowerY: expectedWait, + higherX: next.gasprice, + lowerX: gasprice, + xForExtrapolation: supplementalPrice, + }) + const supplementalPrice2 = getRandomArbitrary(supplementalPrice, next.gasprice) + const supplementalTime2 = extrapolateY({ + higherY: next.expectedWait, + lowerY: supplementalTime, + higherX: next.gasprice, + lowerX: supplementalPrice, + xForExtrapolation: supplementalPrice2, + }) + return [ + { expectedWait, gasprice }, + { expectedWait: supplementalTime, gasprice: supplementalPrice }, + { expectedWait: supplementalTime2, gasprice: supplementalPrice2 }, + ] + } + })) + const withOutliersRemoved = inliersByIQR(withSupplementalTimeEstimates.slice(0).reverse(), 'expectedWait').reverse() + const timeMappedToSeconds = withOutliersRemoved.map(({ expectedWait, gasprice }) => { + const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).toNumber() return { expectedTime, - expectedWait, - gasprice, + gasprice: (new BigNumber(gasprice, 10).toNumber()), } }) const timeRetrieved = Date.now() dispatch(setApiEstimatesLastRetrieved(timeRetrieved)) saveLocalStorageData(timeRetrieved, 'GAS_API_ESTIMATES_LAST_RETRIEVED') - saveLocalStorageData(timeMappedToSeconds.slice(1), 'GAS_API_ESTIMATES') + saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES') - return timeMappedToSeconds.slice(1) + return timeMappedToSeconds }) : Promise.resolve(priceAndTimeEstimates.length ? priceAndTimeEstimates diff --git a/ui/app/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js index dff154dea..4783db30c 100644 --- a/ui/app/ducks/tests/gas-duck.test.js +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -45,10 +45,25 @@ describe('Gas Duck', () => { speed: 'mockSpeed', } const mockPredictTableResponse = [ + { expectedTime: 400, expectedWait: 40, gasprice: 0.25, somethingElse: 'foobar' }, + { expectedTime: 200, expectedWait: 20, gasprice: 0.5, somethingElse: 'foobar' }, { expectedTime: 100, expectedWait: 10, gasprice: 1, somethingElse: 'foobar' }, + { expectedTime: 75, expectedWait: 7.5, gasprice: 1.5, somethingElse: 'foobar' }, { expectedTime: 50, expectedWait: 5, gasprice: 2, somethingElse: 'foobar' }, + { expectedTime: 35, expectedWait: 4.5, gasprice: 3, somethingElse: 'foobar' }, + { expectedTime: 34, expectedWait: 4.4, gasprice: 3.1, somethingElse: 'foobar' }, + { expectedTime: 25, expectedWait: 4.2, gasprice: 3.5, somethingElse: 'foobar' }, { expectedTime: 20, expectedWait: 4, gasprice: 4, somethingElse: 'foobar' }, + { expectedTime: 19, expectedWait: 3.9, gasprice: 4.1, somethingElse: 'foobar' }, + { expectedTime: 15, expectedWait: 3, gasprice: 7, somethingElse: 'foobar' }, + { expectedTime: 14, expectedWait: 2.9, gasprice: 7.1, somethingElse: 'foobar' }, + { expectedTime: 12, expectedWait: 2.5, gasprice: 8, somethingElse: 'foobar' }, { expectedTime: 10, expectedWait: 2, gasprice: 10, somethingElse: 'foobar' }, + { expectedTime: 9, expectedWait: 1.9, gasprice: 10.1, somethingElse: 'foobar' }, + { expectedTime: 5, expectedWait: 1, gasprice: 15, somethingElse: 'foobar' }, + { expectedTime: 4, expectedWait: 0.9, gasprice: 15.1, somethingElse: 'foobar' }, + { expectedTime: 2, expectedWait: 0.8, gasprice: 17, somethingElse: 'foobar' }, + { expectedTime: 1.1, expectedWait: 0.6, gasprice: 19.9, somethingElse: 'foobar' }, { expectedTime: 1, expectedWait: 0.5, gasprice: 20, somethingElse: 'foobar' }, ] const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => { @@ -382,35 +397,13 @@ describe('Gas Duck', () => { [{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 }] ) - assert.deepEqual( - mockDistpatch.getCall(2).args, - [{ - type: SET_PRICE_AND_TIME_ESTIMATES, - value: [ - { - expectedTime: '25', - expectedWait: 5, - gasprice: 2, - }, - { - expectedTime: '20', - expectedWait: 4, - gasprice: 4, - }, - { - expectedTime: '10', - expectedWait: 2, - gasprice: 10, - }, - { - expectedTime: '2.5', - expectedWait: 0.5, - gasprice: 20, - }, - ], + const { type: thirdDispatchCallType, value: priceAndTimeEstimateResult } = mockDistpatch.getCall(2).args[0] + assert.equal(thirdDispatchCallType, SET_PRICE_AND_TIME_ESTIMATES) + assert(priceAndTimeEstimateResult.length < mockPredictTableResponse.length * 3 - 2) + assert(!priceAndTimeEstimateResult.find(d => d.expectedTime > 100)) + assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime)) + assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice)) - }] - ) assert.deepEqual( mockDistpatch.getCall(3).args, [{ type: GAS_ESTIMATE_LOADING_FINISHED }] -- cgit v1.2.3