4
\$\begingroup\$

I've just started learning R, but I feel that I'm using it only as a normal procedural language without taking advantage of some built-in functions and data structures.

I wrote a function that calculates repayments for mortgage with an option of specifying list of overpayments to reduce monthly payments. Please suggest any improvements on making it more native to R.

source("http://pastebin.com/raw.php?i=q7tyiEmM") ## for pmt()
mortgage <- function(price=1000000, deposit=0.10, rate=0.04, years=25, over=c()) {
    loan <- price - price * deposit
    months <- years * 12
    capital <- c()
    interest <- c()
    payments <- c()
    currentCapital <- loan
    monthlyRate <- rate / 12
    payment <- -pmt(monthlyRate, nper = months + 1, pv = loan)

    cat("Initial capital: ", loan, "\n")
    cat("Months: ", months, "\n")
    cat("Payment: ", payment, "\n")

    totalShouldPay <- payment * months
    totalPaid <- 0

    for(currentMonth in 1:months) {
        interest <- currentCapital * monthlyRate
        repaid <- payment - interest
        currentCapital <- currentCapital - repaid

        overPayments <- subset(over, over$month == currentMonth)
        if (length(overPayments) > 0 && nrow(overPayments) > 0) {
            overPayments <- sum(overPayments$payment)
            currentCapital <- currentCapital - overPayments
            payment <- -pmt(monthlyRate, nper = months + 1 - currentMonth, pv = currentCapital)
        }

        capital <- append(capital, currentCapital)

        totalPaid <- totalPaid + payment
        payments <- append(payments, payment)
    }
    cat("Would have paid: ", round(totalShouldPay, 2), "\n")
    cat("Will pay: ", round(totalPaid, 2), "\n")
    cat("Saved: ", round(totalShouldPay - totalPaid, 2), "\n")
    return(data.frame(capital=capital, payments=payments))
}

overPayments <- data.frame(
    month   = c(20,    25,    30,    35,    40), 
    payment = c(20000, 20000, 20000, 20000, 20000))

cap <- mortgage(
    over = overPayments
)

#print(cap)
par(mfcol=c(1, 2))
plot(cap$payments, type="s", main = "Payments", xlab = "Month", ylab = "Payment")
plot(cap$capital, type="l", main = "Capital", xlab = "Month", ylab = "Remaining loan")
\$\endgroup\$
3
  • \$\begingroup\$ In what context does a default price for a mortgage make sense? Is this just for testing, or is price=1000000 an especially common case? \$\endgroup\$ Commented Aug 15, 2014 at 1:46
  • \$\begingroup\$ For payment calculations it doesn't make much difference and just loan would be enough. But I put it there just to see the difference when you decide what initial deposit you would like to put. \$\endgroup\$
    – Dmitry
    Commented Aug 15, 2014 at 12:26
  • 1
    \$\begingroup\$ Is it intentional that you recompute the payment after each iteration? Usually, the monthly payment is fixed at the beginning and remains the same throughout the loan's life. If you ever prepay (your over), then it means your mortgage will end before its scheduled maturity (years), whenever the last payment is greater than currentCapital. This is at least how vanilla fixed mortgages in the US work. I can help you with the code but I need an answer to that question. \$\endgroup\$
    – flodel
    Commented Aug 16, 2014 at 0:42

2 Answers 2

6
\$\begingroup\$

I spent clearly too much time on this... Main ideas:

  1. your understanding of how the payment on a fixed mortgage is computed was wrong. The payment is computed once at the beginning and remains the same regardless of prepayments, which is why people who choose to prepay end up repaying their whole mortgage before the scheduled term, as the example below should show.
  2. I added pmt as an argument to the function as it is best practice for a function to not use any variable defined outside its scope (the dreaded global variables)
  3. A big no-no in your code was the use of constructs like capital <- append(capital, currentCapital). At each iteration, a new and longer vector is created. This is slow and uses a lot of memory. Granted, you would need a very long mortgage to see the negative effects but it's good to learn to implement this the right way. You could a) pre-allocate a vector of length term.in.months and replace its value like this: capital[month.idx] <- currentCapital. Or b) use a list: lists use pointers in memory so they don't suffer the issue I was describing as they grow in size. My implementation uses this b) approach where I grow a list named db. At the end, I am turning this list of monthly data into a data.frame.
  4. You'll see I use things like 12L instead of 12. That's the difference between integers and numerics. It is recommended to use integers when possible as it can prevent (rare) issues regarding floating point errors. It uses less memory and can sometimes make your code faster. Again, you won't notice it here, but it is a good practice.
  5. Regarding @janos' comment about trying to avoid for loops. I agree, R works best and faster when vectorization can replace loops. However, your particular algorithm is a case of an iterative (state i+1 depends on state i) process so it cannot be vectorized. Whereas vectorization can replace loops only in processes where each iteration can be computed independently. So you will find that my answer still has a loop, in the form of a while().
  6. I modified the variables' names to be closer to the official language. (I work in finance.)

mortgage <- function(property.value = 1e6,
                     downpayment = 0.10,
                     interest.rate = 0.04,
                     term = 25L,
                     prepayments = NULL,
                     pmt.fun = pmt) {

   original.balance <- property.value * (1 - downpayment)
   monthly.rate     <- interest.rate / 12L
   term.in.months   <- term * 12L
   monthly.payment  <- -pmt.fun(monthly.rate, nper = term.in.months,
                                              pv = original.balance) 
   db <- list()
   month.idx <- 1L
   start.balance <- original.balance

   while(start.balance > 0) {

      balance.due       <- start.balance * (1 + monthly.rate)
      payment.due       <- min(monthly.payment, balance.due)
      prepayment        <- if (is.null(prepayments)) 0 else
                           min(sum(subset(prepayments, month == month.idx)$payment),
                               balance.due - payment.due)
      total.payment     <- payment.due + prepayment
      interest.payment  <- start.balance * monthly.rate
      end.balance       <- balance.due - total.payment
      principal.payment <- start.balance - end.balance

      db[[month.idx]] <- c(month             = month.idx,
                           start.balance     = start.balance,
                           balance.due       = balance.due,
                           payment.due       = payment.due,
                           total.payment     = total.payment,
                           prepayment        = prepayment,
                           interest.payment  = interest.payment,
                           principal.payment = principal.payment,
                           end.balance       = end.balance)
      month.idx     <- month.idx + 1L
      start.balance <- end.balance
   }
   as.data.frame(do.call(rbind, db))
}

source("http://pastebin.com/raw.php?i=q7tyiEmM")  # for pmt()

overPayments <- data.frame(
   month   = c(20,    25,    30,    35,    40), 
   payment = c(20000, 20000, 20000, 20000, 20000))

cap1 <- mortgage()
cap2 <- mortgage(prepayments = overPayments)

par(mfcol=c(1, 2))
plot(c(cap1$start.balance, 0), type="l",
     main = "Without Prepayments", xlab = "Month", ylab = "Balance")
plot(c(cap2$start.balance, 0), type="l",
     main = "With Prepayments",    xlab = "Month", ylab = "Balance")
\$\endgroup\$
3
  • \$\begingroup\$ I believe it is common for the monthly payment not to be reduced as a result of prepayments. Prepayments go entirely towards paying down the principal. Then, the entire subsequent interest burden goes down, thus shortening the life of the mortgage. \$\endgroup\$ Commented Aug 16, 2014 at 4:15
  • \$\begingroup\$ ...which is exactly what I was saying and have implemented here. \$\endgroup\$
    – flodel
    Commented Aug 16, 2014 at 11:20
  • \$\begingroup\$ wow, thanks a lot, @flodel! That's impressive and gives me a good understanding of a beautiful code in R. Appreciate your time! \$\endgroup\$
    – Dmitry
    Commented Aug 16, 2014 at 15:06
2
\$\begingroup\$

I'm not sure if great improvements will be possible here. The first thing I wanted to attack was the for loop, many times you can achieve the same by converting a for loop to a function and using one of the apply functions to perform operations on rows in a data frame. But it's tricky here, if at all possible. So I will only nitpick on other stuff.


In R, a function returns the last value evaluated by default, so instead of this:

function() {
  # ...
  return(data.frame(capital, payments))
}

You can write like this:

function() {
  # ...
  data.frame(capital, payments)
}

It is a bit controversial topic where you should use an explicit return statement or not. See this related discussion on the topic.

For my part, I like minimalistic code, and omit what is unnecessary, unless there is a good reason not to. In this case, it's easy to omit it, so I would omit.


In a data.frame, the columns will automatically take the names of the vectors you used to build it by default. So instead of:

data.frame(capital=capital, payments=payments)

You can write simply:

data.frame(capital, payments)
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.