R/Escoto.R

library("favir")

PolicyPeriod <- function(written.df, period.dates) {
  # Tag each day in written.df with numeric policy period in period.df
  #
  # Args:
  # written.df - a data frame with the fields date and written
  # period.dates - a vector of dates, indicating the start of each period
  #
  # Output: a vector of period numbers.  Days before the first period
  # get number 0.  The day of the first period until the day before
  # the second period get 1, etc.
  ...
}

InforceDFbyDay <- function(written.df, term.len, term.unit) {
  # Get in force premium by day
  #
  # Args:
  # written.df - a data frame with premium written each day
  #    it should have the fields date, written, and period
  # term.length - the length of the policy term in term.units
  # term.unit - either "day", "month", or "year"
  #
  # Output: a data frame that says how much premium was in force and
  # how much was earned at each day from each policy period.  It will
  # have the fields date, inforce, earned, and period.  There may be
  # multiple rows for each period.
  inforce <- earned <- .InforceDFInitMatrix(written.df, term.len, term.unit)
  for(i in RowIndicies(written.df)) {
    premium <- written.df$written[i]
    term.length <- TermLength(written.df$date[i], term.len, term.unit)
    date.indicies <- i:(i + term.length - 1)
    period <- written.df$period[i]
    inforce[date.indicies, period] <- inforce[date.indicies, period] + premium
    earned[date.indicies, period] <- (earned[date.indicies, period] +
                                        premium / term.length)
  }
  return(.CollapseMatrix(written.df$date[1], inforce, earned))
}

.InforceDFInitMatrix <- function(written.df, term.len, term.unit) {
  # Return a matrix with a row for each date and a column for each period
  last.date <- Last(written.df$date)
  row.num <- nrow(written.df) + TermLength(last.date, term.len, term.unit) - 1
  return(matrix(0, nrow=row.num, ncol=max(written.df$period) ))
}

.CollapseMatrix <- function(start.date, inforce.matrix, earned.matrix) {
  # Return a data frame by removing 0's from matricies
  #
  # The data frame will have an entry for each non-0 row of the
  # matricies.
  all.dates <- seq(start.date, by=1, length.out=nrow(inforce.matrix))
  date <- period <- inforce <- earned <- NULL
  for(i in RowIndicies(inforce.matrix))
    for(j in seq(length.out=ncol(inforce.matrix)))
      if(inforce.matrix[i, j] != 0) {
        date <- c(date, all.dates[i])
        period <- c(period, j)
        inforce <- c(inforce, inforce.matrix[i, j])
        earned <- c(earned, earned.matrix[i, j])
      }
  return(data.frame(date=date, period=period, inforce=inforce, earned=earned))
}

TermLength <- function(date, term.len, term.unit) {
  # Return the length in days of a term starting on the given date.
  #
  # Args: see GetInforce for term.len and term.unit
  #
  # Output: The number of days of a term starting on the given date.
  if(term.unit == "day")
    return(term.len)
  posixlt <- as.POSIXlt(date)
  if(term.unit == "month") {
    tot.months <- posixlt$mon + term.len + 1
    posixlt$mon <- (tot.months %% 12) - 1
    posixlt$year <- posixlt$year + (tot.months %/% 12)
  } else {
    Assert(term.unit == "year", 'Valid units are "day", "month", or "year"')
    posixlt$year <- posixlt$year + term.len
  }
  return(c(as.Date(posixlt) - date)) # unclass before returning
}

EarnedDF <- function(inforce.df, interval.len, interval.unit,
                     term.len, term.unit) {
  # Return a data frame aggregating the inforce into earned premium
  #
  # Args:
  # inforce.df - data frame like the output of InforceDF
  # interval.len - the length of the interval to aggregate to in units
  # interval.unit - either "day", "month", or "year"
  # term.len and term.unit - see InforceDF
  #
  # Output: a data frame giving earned premium by policy period and
  # interval.  Fields are: date, earned, period.
  ...
}

###################################################################

Parallelogram <- function(written.df, rate.change.df, term.len) {
  # Return on-level written and earned premium by interval
  #
  # Args:
  # written.df - a data frame with written premium by period.  Fields are
  #   year and written.
  #
  # rate.change.df - a data frame with dates and rate changes.  Fields
  #   are year and rate.change
  # term.len - length of policy term in years
  written.df <- .CheckWrittenDF(written.df, term.len)
  rate.level.df <- .RateLevelDF(rate.change.df)
  written.steps <- .WrittenSteps(written.df, rate.level.df)
  inforce.pieces <- .InforcePieces(written.steps, term.len)
  result <- list(term.len=term.len, rate.level.df=rate.level.df,
                 written.steps=written.steps,
                 inforce.funcs=lapply(inforce.pieces, function(pair) pair$f),
                 inforce.knots=lapply(inforce.pieces,
                                      function(pair) pair$knots))
  class(result) <- "parallelogram"
  return(result)
}

.CheckWrittenDF <- function(written.df, term.len) {
  # Make sure written.df is in the right format
  if (is.null(written.df)) # Default to constant writing
    return(data.frame(written=1, year=0))
  Assert("data.frame" %in% class(written.df),
         "written.df should be a data frame with premium in it")
  Assert(nrow(written.df) >= 1, "written.df has no rows!")
  Assert("year" %in% names(written.df),
         "No year column found in written.df data frame")
  Assert("written" %in% names(written.df) || "earned" %in% names(written.df),
         'written.df data frame requires either "written" or "earned" column')
  if (!("written" %in% names(written.df)))
    written.df$written <- written.df$earned - term.len / 2
  Assert(!any(is.na(written.df$written)) && !any(is.na(written.df$year)),
         "NAs not allowed inside written.df")
  n <- nrow(written.df)
  if (n > 1)
    Assert(all(written.df$year[2:n] > written.df$year[1:(n - 1)]),
           "years in written.df must be in order")
  return(written.df)
}

.RateLevelDF <- function(rate.change.df) {
  # Check rate.change.df and return rate.level.df
  #
  # rate.level.df will have one row for every different rate level.
  # The columns are:
  #    start - starting time of the period, possibly -Inf
  #    end - end time of the period, possibly Inf
  #    rate.level - the rate level, where 1.0 is the current rate level
  if (is.null(rate.change.df) || nrow(rate.change.df) == 0)
    return(data.frame(start=-Inf, end=Inf, rate.level=1))
  Assert(all(c("year", "rate.change") %in% names(rate.change.df)),
         'rate.change.df must contain "year" and "rate.change" columns')
  rate.change.df <- rate.change.df[order(rate.change.df$year), ]
  start <- c(-Inf, rate.change.df$year)
  end <- c(rate.change.df$year, Inf)
  rate.level <- c(rev(cumprod(rev(1 + rate.change.df$rate.change))), 1.0)
  return(data.frame(start=start, end=end, rate.level=rate.level))
}

.WrittenSteps <- function(written.df, rate.level.df) {
  # Return list of step functions, one for each rate change period
  Helper <- function(start, end) {
    # Return a single step function covering written from start to end
    prev.written <- if (any(written.df$year < start))
      written.df[max(which(written.df$year < start)), "written"]
    else written.df$written[1]
    sub.df <- written.df[start <= written.df$year & written.df$year < end, ]
    return(stepfun(x=c(start, sub.df$year, end),
                   y=c(0, prev.written, sub.df$written, 0)))
    
  }
  return(mlply(rate.level.df[, c("start", "end")], Helper))
}

.InforcePieces <- function(written.steps, term.len) {
  # Return list of inforce information, one element for each rating period
  #
  # Each element in the result be a list with two subelements:
  #   f - a piecewise linear functions giving inforce premium at that time
  #   knots - a numeric vector of f's turning points
  Helper <- function(stepfun) {
    # Return f and knots for a single step function
    f.knots <- GetKnots(stepfun)
    if (length(f.knots) == 0) # written and inforce are constant
      return(list(f=function(x) stepfun(0) * term.len, knots=NULL))
    y <- YVals(f.knots, stepfun)
    f <- approxfun(f.knots, y, rule=2, method="linear")
    return(list(knots=f.knots, f=f))
  }
  
  GetKnots <- function(stepfun) {
    # Return the knots of the inforce premium function given written step fun
    f.knots <- sort(unique(c(knots(stepfun), knots(stepfun) + term.len)))
    f.knots <- f.knots[-Inf < f.knots & f.knots < Inf]
    Assert(length(f.knots) >= 2, "Sanity check--this shouldn't be false")
    return(f.knots)
  }
  
  YVals <- function(f.knots, stepfun) {
    # Given knots of inforce function and stepfun, return inforce prem function
    #
    # This integrates the stepfun by rolling over the interval (x -
    # term.len, x) one unit at a time (once per iteration of the for
    # loop).
    x <- f.knots[1]
    y <- stepfun(x - 1) * term.len # stepfun is constant before first knot
    for (new.x in f.knots[2:length(f.knots)]) {
      new.inforce <- stepfun(Last(x)) * (new.x - Last(x))
      expired <- stepfun(Last(x) - term.len) * (new.x - Last(x))
      x <- c(x, new.x)
      y <- c(y, Last(y) + new.inforce - expired)
    }
    return(y)
  }
  
  return(lapply(written.steps, Helper))
}

TotalWrittenFunc <- function(parallelogram) {
  # Return a function yielding total written premium given year
  Assert("parallelogram" %in% class(parallelogram),
         "First argument of TotalWrittenFunc should be parallelogram object")
  written.funcs <- parallelogram$written.steps
  ReturnFunc <- function(years) {
    # Return total written for each year specified
    result <- rep(0, length(years))
    for (i in seq(along=written.funcs))
      result <- result + written.funcs[[i]](years)
    return(result)
  }
}

TotalInforceFunc <- function(parallelogram) {
  # Return function that yields total inforce premium as function of time
  Assert("parallelogram" %in% class(parallelogram),
         "First argument of TotalInforceFunc should be parallelogram")
  inforce.funcs <- parallelogram$inforce.funcs
  ReturnFunc <- function(years) {
    # Return total inforce for each year specified
    result <- rep(0, length(years))
    for (i in seq(along=inforce.funcs))
      result <- result + inforce.funcs[[i]](years)
    return(result)
  }
  return(ReturnFunc)
}

EarnedByPeriod <- function(parallelogram, periods.out) {
  # Return earned premium by period from parallelogram results
  #
  # Inputs:
  #   parallelogram - parallelogram results object
  #   periods.out - vector of period begin/end numbers
  # Output will be a data frame with these columns:
  #   start, end - the year of the start and end of the period.out
  #   rating.period - the number of the rating period
  #   earned - the premium earned in that out period that was written under
  #     that rating period.
  Helper <- function(period.df) {
    # Given start and end times for the period, return section of final result
    Assert(nrow(period.df) == 1, "Sanity Check")
    
    RatingHelper <- function(rating.period) {
      # Returned earned premium during the period for a single rating period
      #
      # To do this we integrate over the piecewise linear inforce
      # function.  Since we know where the knots are, to find the area
      # we can just average the beginning and end of each section and
      # multiply by the width (each section is trapezoidal).
      knots <- parallelogram$inforce.knots[[rating.period + 1]]
      knots <- knots[period.df$start < knots & knots < period.df$end]
      Inforce <- parallelogram$inforce.funcs[[rating.period + 1]]
      oldx <- period.df$start
      area <- 0
      for (newx in c(knots, period.df$end)) {
        area <- area + (Inforce(oldx) + Inforce(newx)) / 2 * (newx - oldx)
        oldx <- newx
      }
      return(area / parallelogram$term.len)
    }
    
    rating.periods <- RowIndicies(parallelogram$rate.level.df) - 1 # start at 0
    earned <- sapply(rating.periods, RatingHelper)
    return(data.frame(rating.period=rating.periods, earned=earned))
  }
  
  Assert("parallelogram" %in% class(parallelogram),
         "First parameter should be a parallelogram as made by Parallelogram")
  periods.df <- .CheckPeriodsOut(periods.out)
  return(ddply(periods.df, .(start, end), Helper))
}

.CheckPeriodsOut <- function(periods.out) {
  # Make sure periods.out is in right format, return periods.df
  n <- length(periods.out)
  Assert(n >= 2, "periods.out requires at least beginning and end points")
  Assert(all(periods.out[2:n] > periods.out[1:(n - 1)]),
         "times in periods.out need to be in order")
  Assert(!any(is.na(periods.out)), "NA's not allowed in periods.out")
  return(data.frame(period.out=1:(n - 1),
                    start=periods.out[1:(n - 1)], end=periods.out[2:n]))
}

OLEP <- function(parallelogram, periods.out, earned.df=NULL) {
  # Return On-Level Earned Premium by period
  if (is.null(earned.df))
    earned.df <- EarnedByPeriod(parallelogram, periods.out)
  earned.df$rate.level <- parallelogram$rate.level.df$rate.level[
    earned.df$rating.period + 1]
  return(daply(earned.df, .(start),
               function(df) sum(df$earned * df$rate.level)))
}

OLEF <- function(parallelogram, periods.out, earned.df=NULL) {
  # Return on-level premium factors by specified period
  if (is.null(earned.df))
    earned.df <- EarnedByPeriod(parallelogram, periods.out)
  earned.df$rate.level <- parallelogram$rate.level.df$rate.level[
    earned.df$rating.period + 1]
  Helper <- function(df) sum(df$earned * df$rate.level) / sum(df$earned)
  return(daply(earned.df, .(start), Helper))
}

EarnedPremium <- function(parallelogram, periods.out, earned.df=NULL) {
  # Return raw earned premium by period
  if (is.null(earned.df))
    earned.df <- EarnedByPeriod(parallelogram, periods.out)
  return(daply(earned.df, .(start), function(df) sum(df$earned)))
}

SimpleOLEF <- function(rate.change.df, periods.out, term.len=1) {
  # A convenience function to compute the on-level factors for the given periods
  p <- Parallelogram(written.df=NULL, rate.change.df, term.len)
  return(OLEF(p, periods.out))
}
PirateGrunt/OnLevel documentation built on May 8, 2019, 3:19 a.m.