@wmfs/tymly-rankings-plugin
Version:
Plugin which handles ranking of data for Tymly framework
143 lines (117 loc) • 4.62 kB
JavaScript
const _ = require('lodash')
const dist = require('distributions')
const calculateNewRiskScore = require('./calculate-new-risk-score')
const projectedRecoveryDates = require('./projected-recovery-dates')
const debug = require('debug')('tymly-rankings-plugin')
const toTwoDp = require('./to-two-dp')
module.exports = async function generateStats (options) {
debug(options.category + ' - Generating statistics')
const scores = await loadRiskScores(options)
if (scores.length === 0) {
debug(options.category + ' - No scores found')
} // if ...
const { mean, stdev, ranges } = await getStats(scores, options)
const fsRanges = options.registry.value.exponent
const normal = calculateDistribution(mean, stdev, options)
await moveCalculatedRiskScoresAlongGrowthCurve(scores, mean, stdev, ranges, normal, fsRanges, options)
} // generateStats
async function loadRiskScores (options) {
const result = await getScores(options)
return result.rows.map(r => {
const original = (r.original_risk_score === 0 || r.original_risk_score === 1) ? 2 : r.original_risk_score
return {
uprn: r.uprn,
original
}
})
} // loadRiskScores
async function getStats (scores, options) {
const statsRes = await options.client.query(`select mean::float, stdev::float, ranges from ${options.statsViewKey} where category = '${_.kebabCase(options.category)}'`)
const { mean, stdev } = statsRes.rows[0]
const ranges = statsRes.rows[0].ranges || {}
ranges.find = score => {
for (const [name, range] of Object.entries(statsRes.rows[0].ranges)) {
if (score >= range.lowerBound && score <= range.upperBound) {
return name
}
}
}
return { mean, stdev, ranges }
} // getStats
function calculateDistribution (mean, stdev, options) {
debug(options.category + ' - Calculating distributions')
if (stdev === 0 || stdev === null || mean === null || isNaN(stdev) || isNaN(mean)) {
return null
}
return dist.Normal(mean, stdev)
} // calculateDistribution
function getScores (options) {
return options.client.query(`SELECT ${_.snakeCase(options.pk)}, original_risk_score::float FROM ${_.snakeCase(options.schema)}.${_.snakeCase(options.category)}_scores`)
}
function distPdf (normal, score) {
if (normal === null) {
return NaN
}
const pdf = normal.pdf(score)
return Math.round(pdf * 10000) / 10000
}
async function moveCalculatedRiskScoresAlongGrowthCurve (scores, mean, stdev, ranges, normal, fsRanges, options) {
debug(options.category + ' - Moving calculated risk score along growth curve')
for (const s of scores) {
const row = await options.rankingModel.findById(s.uprn)
if (!(row.lastAuditDate && row.fsManagement)) {
await setRankingFromOriginalScore(s, ranges, normal, options)
} else {
await moveAlongGrowthCurve(s, row, mean, stdev, ranges, normal, fsRanges, options)
}
}
} // moveCalculatedRiskScoresAlongGrowthCurve
async function setRankingFromOriginalScore (score, ranges, normal, options) {
const range = ranges.find(score.original)
await options.rankingModel.upsert({
[options.pk]: score[_.snakeCase(options.pk)],
rankingName: _.kebabCase(options.category),
range: _.kebabCase(range),
distribution: distPdf(normal, score.original),
originalRiskScore: score.original
}, {
setMissingPropertiesToNull: false
})
} // setRankingFromOriginalScore
async function moveAlongGrowthCurve (score, row, mean, stdev, ranges, normal, fsRanges, options) {
const daysSinceAudit = options.timestamp.today().diff(row.lastAuditDate, 'days')
const exp = fsRanges[row.fsManagement]
const originalRange = ranges.find(score.original)
const crs = calculateNewRiskScore(
score.original,
originalRange,
daysSinceAudit,
mean,
stdev,
exp
) // updatedRiskScore
const updatedRiskScore = toTwoDp(crs)
const newRange = ranges.find(updatedRiskScore)
const { projectedHighRiskDate, projectedReturnDate } = projectedRecoveryDates(
score.original,
originalRange,
ranges,
daysSinceAudit,
mean,
stdev,
exp,
options.timestamp.today()
)
await options.rankingModel.upsert({
[options.pk]: score[_.snakeCase(options.pk)],
rankingName: _.kebabCase(options.category),
range: _.kebabCase(newRange),
distribution: distPdf(normal, updatedRiskScore),
originalRiskScore: score.original,
updatedRiskScore: updatedRiskScore,
projectedHighRiskCrossover: projectedHighRiskDate,
projectedReturnToOriginal: projectedReturnDate
}, {
setMissingPropertiesToNull: false
})
} // moveAlongGrowthCurve