Published
Edited
Jul 25, 2022
1 star
Insert cell
Insert cell
Insert cell
// Use this object to customize forecasting behaviour.
forecastOptions = ({
// Budget managers can manually predict next year's uplift, but we may want to ignore that in some cases.
ignoreExpectedUplift: false,
// We may not want to look at the entire history. To pick up on smaller trends for instance, we may only want to look at the last 2 years.
lookbackYears: 3,

// Set to false to prevent the forecaster from predicting decreases in cost.
predictDecreases: true,
decreaseLimit: 0
})
Insert cell
// Predict the cost of a renewal, given data from past renewals.
forecast = function(renewals, year) {
if (!Array.isArray(renewals)) return 0;

// Remove poorly formatted/null entries.
renewals = renewals.filter((r) => {
return typeof r.year === 'number' && typeof r.cost === 'number';
});

// Limit data to within lookbackYears.
if (typeof forecastOptions.lookbackYears == 'number' && forecastOptions.lookbackYears > 0) {
renewals = renewals.slice((forecastOptions.lookbackYears * -1))
}

// Remove uplift predictions if the configuration calls for it.
if (forecastOptions.ignoreExpectedUplift) {
renewals.forEach((r) => { delete r.expectedUplift; });
}
if (renewals.length < 1) {
return 0;
} else if (renewals.length == 1 || typeof renewals[renewals.length - 1].expectedUplift !== 'undefined') {
// An expectedUplift was provided, which will be a better estimate than a linear regression.
// Return a linear (y=mx+b) estimate, using cost, and expectedUplift.
let index = renewals.length - 1;
let m = (1.0 + renewals[index].expectedUplift ? renewals[index].expectedUplift : 0) * renewals[index].cost;
let x = year - renewals[index].year;
let b = renewals[index].cost;
return (m * x) + b;
} else {
// No expectedUplift was provided, but we have enough historical data to form a linear regression.
// First, find the slope.
// For the slope of a linear regression, s = sum((x-avgx)(y-avgy)) / sum((x-avgx)^2)
let totalYear = 0;
let totalCost = 0;
renewals.forEach((r) => { totalYear += r.year; });
var avgYear = totalYear / renewals.length;
renewals.forEach((r) => { totalCost += r.cost; });
var avgCost = totalCost / renewals.length;

let numerator = 0;
let denominator = 0;
renewals.forEach((r) => {
numerator += (r.year - avgYear) * (r.cost - avgCost);
denominator += (r.year - avgYear) * (r.year - avgYear);
});
var slope = numerator / denominator;
if (Number.isNaN(slope)) slope = 0;
// Now find the y intercept.
var yIntercept = avgCost - (slope * avgYear);
// Now we have slope and y intercept. The user provided the year, so we can solve for cost.
var cost = slope * year + yIntercept;

if (!forecastOptions.predictDecreases) {
// Dont predict too low, or we'll go over budget.
if (cost < (renewals[renewals.length - 1].cost - forecastOptions.decreaseLimit)) {
return renewals[renewals.length - 1].cost - forecastOptions.decreaseLimit;
}
}
return cost;
}
}
Insert cell
Insert cell
// Get an estimate for a specific expense, during a specific year, using the forecast function.
forecast(getExpense(1).renewals, 2025);
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more