Hello, I have written a piece of code that makes use of some conditional statements, but I feel like I am using too many nested if statements. Most of these conditional statements are simply changing the bounded interval over which a function is being called (or optimized). I think some these can be replaced by a while or for loop but I have not been to figure out how to do it in this case.
How can I use a loop here or any better formulation? Would appreciate any help!
using Optim
function npv(irr,cf,period)
sum(cf[i]/(1+irr)^period[i] for i in 1:length(cf))
end
function irr(cf)
if cf[1]==0 && any(x->abs(x)>0,cf[2:end])
R = Inf
elseif abs(cf[1])>0 && all(x->x==0,cf[2:end])
R = NaN
else
period = collect(0:length(cf)-1)
f(x) = npv(x,cf,period)
result = optimize(x -> f(x)^2,0.0,1.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
R= Optim.minimizer(result)
else result = optimize(x -> f(x)^2,1.0,100000000.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
R = Optim.minimizer(result)
else result = optimize(x -> f(x)^2,-1.0,0.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
R= Optim.minimizer(result)
else result = optimize(x -> f(x)^2,-100000000.0,0.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
R= Optim.minimizer(result)
else
R= NaN
end
end
end
end
end
return R
end
One really simple way to clean this up is to remove all of the else statements. That transforms this into
function irr(cf)
if cf[1]==0 && any(x->abs(x)>0,cf[2:end])
return Inf
if abs(cf[1])>0 && all(x->x==0,cf[2:end])
return NaN
end
period = collect(0:length(cf)-1)
f(x) = npv(x,cf,period)
result = optimize(x -> f(x)^2,0.0,1.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
return Optim.minimizer(result)
end
result = optimize(x -> f(x)^2,1.0,100000000.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
return Optim.minimizer(result)
end
result = optimize(x -> f(x)^2,-1.0,0.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
return Optim.minimizer(result)
end
result = optimize(x -> f(x)^2,-100000000.0,0.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
return Optim.minimizer(result)
end
return NaN
end
Thank you! Do you have any suggestions on how a loop can be used here? I think a loop will give me more flexibility to iterate through an arbitrary number of bounds.
Wouldn’t removing else require the function to be evaluated again and again even if the previous condition was satisfied? For eg if the first evaluation of the function satisfies the criteria then it is unnecessary to call the function again to be optimized.
I have updated the code to get rid of return from if statements (see above). Hopefully it should exit the if statements now as soon as the condition is satisfied, but I am novice in Julia so can’t be sure.
Sorry, that was a really confusing statement. What I meant by you never exit out is that you never execute code in the function after the return if you end up in the if statement. You still want the returns in this program to exit out of the function.
If I understand correctly what you are doing, it’s basically two steps:
Find the non-zero values of cf that will be actually used in the npv calculations.
Run the optimizer on npv to find the irr. This is done over various predefined intervals until a solution is found.
I would factor out the first part into a function that returns the non-zero indices of cf (where checking abs(x)>0 seems equivalent to x!=0).
Then dispatch: if only the first index is non-zero, return NaN. If the first is zero return Inf. What if all are zero? Else do the optimization.
For the optimization, I would set up the bounds as a Vector of Vectors (or similar). Then iterate over the bounds until optimize returns something near 0. This way, you don’t need all the if Optim.minimum(result) statements and the optimize code does not need to be repeated.
In the irr(cf) function, the first if and elseif statements are intended to exclude the cases where there will be no solution. Specifically, if the first element in cf is zero and there are any other non-zero elements, then it should return Inf. If the first element in cf is non-zero but the remainder are zeros then it should return NaN.
Now, if these two conditional statements are not true then the optimizer is run on npv to find irr. The npv will use all elements of cf including zeros (of course excluding the cases that would fall in the first two conditional statements), e.g.
I should have added an if statement to return NaN or a numerical error in this case.
The optimizer may fail to converge if the bounds are set too wide or give a negative result when a positive result is possible. This is the reason I am changing the bounds.
This is the bit I am struggling with. I can define the bounds as below, but I do not understand how to iterate over them until the convergence criteria is satisfied, probably a while loop is required. How should I use the loop?
Thank you. With your code, the function does not handle the case where all elements of cf are positive or all elements negative, e.g irr([100,100,101,10]) hangs the program, whereas my original code above would return NaN. I have given my full code based on your suggestion below.
I do not understand this part. Do we need a break statement here?
using Optim
function npv(irr,cf,period)
sum(cf[i]/(1+irr)^period[i] for i in 1:length(cf))
end
function irr(cf)
if cf[1]==0 && any(x->x!=0,cf[2:end])
return Inf
elseif abs(cf[1])>0 && all(x->x==0,cf[2:end])
return NaN
elseif all(x->x==0,cf)
return NaN
else
period = collect(0:length(cf)-1)
f(x) = npv(x,cf,period)
lb= [0.0,1.0,-1.0,-1.0e8]
ub=[1.0,1.0e8,0.0,0.0]
done = false;
j = 1;
while !done
result = optimize(x -> f(x)^2, lb[j], ub[j], Brent());
if Optim.minimum(result) <= 1e-9
done = true
return Optim.minimizer(result)
elseif j == length(lb)
# handle that error
else
j += 1;
end
end
end
end
I was just sketching this. The following looks complete to me:
using Optim
# This is, btw not efficient given that you only use consecutive periods.
# Instead of the `^period[i]` it would be more efficient to loop as in
# pv = 0.0
# R = 1 + irr
# cumR = R
# for j = 1 : length(cf)
# cumR /= R
# pv += cf[j] / cumR
# end
function npv(irr,cf,period)
sum(cf[i]/(1+irr)^period[i] for i in 1:length(cf))
end
function irr(cf)
# Get non-zero indices
idx = findall(x -> x != 0.0, cf);
# Make sure solution may exist
if length(idx) < 2
return NaN
elseif !(any(x -> x > 0.0, cf) && any(x -> x < 0.0, cf))
return NaN
end
lb= [0.0,1.0,-1.0,-1.0e8]
up=[1.0,1.0e8,0.0,0.0]
done = false;
j = 1;
result = 0.0;
f(x) = npv(x, cf[idx], 0 : length(idx) - 1)
while !done
result = optimize(x -> f(x)^2, lb[j], up[j], Brent());
if Optim.minimum(result) <= 1e-9
done = true
elseif j == length(lb)
# No more bounds to try. Solution does not exist.
done = true
result = NaN
else
# Next set of bounds to try
j += 1;
end
end
return result
end
irr([-100,10,110])
Instead of checking whether the first entry is non-zero etc, I am simply checking whether there are any positive and negative entries.
I will change this line of code to return Optim.minimizer(result)
Using a loop makes the npv function slightly faster. The interesting thing that I observed was if I use @floop macro before the for loop it makes it a bit slower.
function npv_loop(irr,cf,period)
pv = 0.0
R = 1 + irr
cumR = R
for j = 1 : length(cf)
cumR /= R
pv += cf[j] / cumR
end
return pv
end
using FLoops
function npv_Floop(irr,cf,period)
@floop begin
pv = 0.0
R = 1 + irr
cumR = R
for j = 1 : length(cf)
cumR /= R
pv += cf[j] / cumR
end
end
return pv
end