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))
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.