Nested if statements better formulation

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

Example usage irr([-100,10,110])

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
3 Likes

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.

Once you return from a function, you’re out of the function. Since all of your if statements have returns, you never exit out of them.

1 Like

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.

2 Likes

If I understand correctly what you are doing, it’s basically two steps:

  1. Find the non-zero values of cf that will be actually used in the npv calculations.
  2. 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.

2 Likes

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.

julia> irr([-100,0,10,10,0,0,110])
0.049594202242656926

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?

lb= [0.0,1.0,-1.0,-1.0e8]  
up=[1.0,1.0e8,0.0,0.0] 
bounds = hcat(lb,up)

I see - zeros are valid values.

As for the bounds:

lb= [0.0,1.0,-1.0,-1.0e8]  
up=[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
  elseif j == length(lb)
    # handle that error
  else
    j += 1;
  end
end
2 Likes

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.

Thanks you!

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
@btime npv_loop(0.01,[10,10,10],[1,2,3])
  72.661 ns (2 allocations: 224 bytes)
30.301000000000002
@btime npv_Floop(0.01,[10,10,10],[1,2,3])
  76.700 ns (2 allocations: 224 bytes)
30.301000000000002