Reputation: 131
I need to find a number (my guess) that when used in a simple calculation will produce a specific sum (target). To that end I have written a calcGuess function. It works, but I would like to reduce the number of iterations needed to calculate my guess.
My approach involves adding or subtracting a modifier to the guess for reach iteration.
If the previous iteration's guess went above the target number, then I will divide the modifier in two and subtract it from my guess.
If the previous iteration's guess went below the target number, then I will divide the modifier in two and add it to my guess.
If the previous iteration didn't go above or below the target number, but brought me closer to the target number, then I will keep using the modifier without changing it.
Any suggestions to increase the efficiency of the guessing algorithm will be greatly appreciated.
var _target = 40000;
var _cashflow = [1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1398];
var _guess = 0.0014534646484;
var _modifier = 0.01;
var correctGuess = calcGuess(_target, _cashflow, _guess, _modifier, false, false, false, 0);
function calcGuess(target, cashflow, guess, modifier, hasPrevGuess, prevGuessToHigh, prevGuessToLow, iterations) {
// prevent an infinite loop
if (++iterations > 100) {
console.log('Kill switch');
return;
}
// calculate a new target using the cashflow array and our guess
var targetUsingGuess = 0;
for (var n = 0; n < cashflow.length; n++) {
targetUsingGuess += cashflow[n] / Math.pow(1 + guess, n);
}
// compare our new target to the original target
if (targetUsingGuess.toFixed(8) == target.toFixed(8)) {
// the guess matched!
console.log(iterations, ' Matched!: ', guess);
return guess;
}
else {
// the guess was too high
if (targetUsingGuess > target) {
// this where I modify my guess and
// this is part which I think can be much efficient
if (hasPrevGuess && prevGuessToLow) {
modifier /= 2;
}
guess += modifier;
console.log(iterations, ' Too high: ', guess);
return calcGuess(target, cashflow, guess, modifier, true, true, false, iterations);
}
// the guess was too low
if (targetUsingGuess < target) {
// this where I modify my guess and
// this is part which I think can be much efficient
if (hasPrevGuess && prevGuessToHigh) {
modifier /= 2;
}
guess -= modifier;
console.log(iterations, ' Too low: ', guess);
return calcGuess(target, cashflow, guess, modifier, true, false, true, iterations);
}
}
}
Upvotes: 0
Views: 111
Reputation: 7579
Since we have the result and want to find the input to a function for which you obtain the result, the Newton-Raphson method of finding roots will be the best option.
Your required target T
has to be calculated from the function t(x) = sum of C[i] / (1 + x)^i
, where 0 <= i < n
. C
represents your cashflow
array, n
represents cashflow.length
and x
is your guess. The function can be rewritten as follows: t(x) = sum of C[i] * (1 + x)^(-i)
. If all C[i]
are equal, the function can be reduced to a rational function using the sum of a geometric series, but since you have a cashflow array, I am going to assume that they all are different and leave t(x)
as is.
Your problem now reduces to finding g
(the guess) for which t(g) = T
.
NOTE: For the given function, the obvious vertical asymptote is x = -1
. Due to the nature of the sum, for any T > C[0]
(the first element in the cashflow array), g > -1
. I will use this assumption that the desired target will always be greater than the first element of the cashflow array throughout. The given method will work for T < C[0]
too with a slight change in the initial guess, but will eventually cause a division by zero if T
is less than or equal to the minimum of the function for odd n
. f(x)
is guaranteed to be monotonic for x > -1
, but monotonicity in x < -1
is guaranteed only for even n
.
The working algorithm for the Newton-Raphson method of finding roots is as follows:
If x(i)
is the i
th guess,
x(i+1) = x(i) - f(x(i)) / f'(x(i))
Where f'(x)
is the derivative of f(x)
.
In our case, f(x) = t(x) - T
since we have to find the value of x
for which t(x) = T => t(x) - T = 0 => f(x) = 0
.
From our definition of f(x)
, f'(x) = t'(x) = sum of C[i] * (-i) * (1 + x)^(-i - 1)
.
The only thing remains is to find a good initial guess. The nature of the function combined with our assumption allows us to give a very easy answer to this question. Since the function is decreasing for x > -1
, we simply take the smallest number greater than -1
. From your code, it looks like your error tolerance (TOL
) is 1e-8
(the first 8 decimal digits must be the same). Therefore, the initial guess is -1 + TOL
.
You might argue that since the function is monotonic for x > -1
, we can choose any x > -1
as the initial guess, but due to the discontinuous nature of the function, guesses greater than a specific value depending on the function definition will cause the next guess to be less than -1
, and subsequent guesses will diverge to infinity instead of converging.
EDIT: After some testing, 1 - target / sum(cashflow)
makes a much much better initial guess, converging in 10-20 iterations vs. -1 + TOL
's convergence in ~500 iterations.
The algorithm must be terminated when the absolute difference between two guesses is less than TOL
, but here we terminate if the absolute difference between the target and the guessed result (simply abs(f(guess))
) is less than TOL
since the rate of convergence for some cases may be very slow.
var _target = 40000;
var _cashflow = [1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1934, 1398];
var _tolerance = 1e-8;
var correctGuess = calcGuess(_target, _cashflow, _tolerance);
console.log("Final guess:", correctGuess);
function calcGuess(target, cashflow, tolerance) {
var f = function(x) {
var sum = 0.0;
for (var i = 0; i < cashflow.length; i++) {
sum += cashflow[i] * Math.pow(1 + guess, -i);
}
return sum - target;
}
// Derivative of f
var df = function(x) {
var sum = 0.0;
for (var i = 0; i < cashflow.length; i++) {
sum += cashflow[i] * (-i) * Math.pow(1 + guess, -i - 1);
}
return sum;
}
// Initial guess
var guess = 1 - target / cashflow.reduce(function(a, b) { return a + b; }, 0);
// Newton-Raphson
for (var iter = 0; iter < 1000; iter++) {
guess -= f(guess) / df(guess);
if(Math.abs(f(guess)) < tolerance) {
// Found guess, return
break;
}
console.log(iter, ":", guess);
}
console.log("Difference:", f(guess));
return guess;
}
Upvotes: 2