#' Interal rate of return
#'
#' This function returns a data frame that produces
#' the \href{https://en.wikipedia.org/wiki/Internal_rate_of_return}{internal rate of return}
#' for specified dates and cash flows
#'
#' @param cash_flows vector of cash flows
#' @param dates vector of dates, year-month-date format
#' @param date_format date format
#' @param scale_to_100 \code{TRUE} scale to 100
#' @param return_percentage \code{TRUE} return percentages
#' @param return_df \code{TRUE} returns a data frame
#' @param return_message \code{TRUE} return a message after data import
#' @importFrom magrittr %>%
#' @importFrom formattable digits currency percent
#' @importFrom lubridate ymd
#' @importFrom dplyr tibble
#'
#' @return \code{tibble}
#' @family calculation
#' @family leveraged finance calculation
#' @family partnership calculation
#' @export
#'
#' @examples
#' calculate_irr_periods(dates = c("2016-06-01","2017-05-31", "2018-05-31", "2019-05-31", "2020-05-31", "2021-05-31",
#' "2022-05-31", "2023-05-31", "2024-05-31", "2025-05-31", "2026-05-31"), cash_flows = c( -3000, 478.515738547242, 478.515738547242, 478.515738547242, 478.515738547242,
#' 478.515738547242, 478.515738547242, 478.515738547242, 478.515738547242, 478.515738547242, 478.515738547278 ), date_format = '%Y-%m-%d', scale_to_100 = FALSE,
#' return_percentage = FALSE, return_df = TRUE, return_wide = TRUE, return_message = TRUE)
calculate_irr_periods <-
function(dates = c(
"2016-06-01",
"2017-05-31",
"2018-05-31",
"2019-05-31",
"2020-05-31",
"2021-05-31",
"2022-05-31",
"2023-05-31",
"2024-05-31",
"2025-05-31",
"2026-05-31"
),
cash_flows = c(
3000,
-478.515738547242,
-478.515738547242,
-478.515738547242,
-478.515738547242,
-478.515738547242,
-478.515738547242,
-478.515738547242,
-478.515738547242,
-478.515738547242,
-478.515738547278
),
date_format = '%Y-%m-%d',
scale_to_100 = F,
return_percentage = F,
return_df = T,
return_wide = T,
return_message = T) {
secant <-
function(par,
fn,
tol = 1.e-07,
itmax = 100,
trace = TRUE,
...) {
# par = a starting vector with 2 starting values
# fn = a function whose first argument is the variable of interest
#
if (length(par) != 2)
stop("You must specify a starting parameter vector of length 2")
p.2 <- par[1]
p.1 <- par[2]
f <- rep(NA, length(par))
f[1] <- fn(p.1, ...)
f[2] <- fn(p.2, ...)
iter <- 1
pchg <- abs(p.2 - p.1)
fval <- f[2]
if (trace)
# cat("par: ", par, "fval: ", f, "\n")
while (pchg >= tol & abs(fval) > tol & iter <= itmax) {
p.new <- p.2 - (p.2 - p.1) * f[2] / (f[2] - f[1])
pchg <- abs(p.new - p.2)
fval <- fn(p.new, ...)
p.1 <- p.2
p.2 <- p.new
f[1] <- f[2]
f[2] <- fval
iter <- iter + 1
}
list(par = p.new,
value = fval,
iter = iter)
}
npv <-
function (irr, cashFlow, times)
sum(cashFlow / (1 + irr) ^ times)
cfDate <-
dates %>%
as.Date(format = date_format)
times <-
difftime(cfDate, cfDate[1], units = "days") %>% as.numeric() / 365.24
s <-
secant(
par = c(0, 0.1),
fn = npv,
cashFlow = cash_flows,
times = times
)
irr <-
s$par
if (return_percentage == T & scale_to_100 == T) {
stop("Sorry you cannot return a percentage and scale to 100")
}
if (return_percentage) {
irr <-
irr %>% formattable::percent()
}
if (scale_to_100) {
irr <-
irr * 100
}
dateStart <-
min(dates) %>% ymd
dateEnd <-
max(dates) %>% ymd
equityContributions <-
cash_flows[cash_flows > 0] %>%
sum() %>%
formattable::currency(digits = 2)
equityDistributions <-
cash_flows[cash_flows < 0] %>%
sum() %>%
formattable::currency(digits = 2)
multipleCapital <-
(abs(equityDistributions) / equityContributions) %>% digits(digits = 3)
amountProfit <-
-(equityDistributions + equityContributions)
if (return_df)
data <-
tibble(
dateStart,
dateEnd,
equityContributions,
equityDistributions,
pctIRR = irr,
amountProfit,
multipleCapital,
dateTimeCF = Sys.time()
) %>%
mutate(pctIRR = percent(pctIRR, digits = 3))
else {
data <-
irr
}
if (return_message) {
"\nCash Flow Produces a " %>%
paste0(
formattable::percent(irr, digits = 3),
' IRR\nFrom ',
dateStart,
' to ',
dateEnd,
'\n',
'Profit of ',
formattable::currency(amountProfit, digits = 2),
'\nCapital Multiple of ',
multipleCapital,
"\n"
) %>%
cat(fill = T)
}
if (!return_wide) {
data <-
data %>%
gather(metric, value, -c(dateStart, dateEnd, dateTimeCF))
}
return(data)
}
#' Summary cash flows
#'
#' This function returns a data frame of
#' summarised cash flows for given a set of inputs.
#'
#' @param dates vector of dates
#' @param cash_flows vector of cash flows
#' @param working_capital amount of working capital, minimum cash
#' @param remove_cumulative_cols \code{TRUE} remove summary columns
#' @param include_final_day \code{TRUE} include the final day in calculation
#' @param distribution_frequency frequency of distribution \itemize{
#' \\item \code{NA}: NA
#' \item \code{weekly}: weekly distributions
#' \item \code{monthly}: monthly distributions
#' \item \code{quarterly}: quarterly distributions
#' \item \code{annually}: annual distributions
#' \item \code{sale}: distribution on residual
#' }
#' @import tidyr dplyr stringr formattable purrr
#' @importFrom magrittr %>%
#' @importFrom lubridate ymd
#' @return tibble
#' @export
#' @family calculation
#' @family leveraged finance calculation
#' @family partnership calculation
#' @examples
#' calculate_cash_flow_dates(dates = c( "2016-09-01", "2017-08-31", "2018-08-31", "2019-08-31", "2020-08-31", "2021-08-31", "2022-08-31", "2023-08-31" ),
#' cash_flows = c( -4151601, 119499.036215643, 257186.036215643, 447646.036215643, 200652.036215643, 510409.036215643, 193.036215643166, 8788626.7640915 ),
#' working_capital = 125000, remove_cumulative_cols = TRUE, include_final_day = TRUE, distribution_frequency = NA)
calculate_cash_flow_dates <-
function(dates = c(
"2016-09-01",
"2017-08-31",
"2018-08-31",
"2019-08-31",
"2020-08-31",
"2021-08-31",
"2022-08-31",
"2023-08-31"
),
cash_flows = c(
-4151601,
119499.036215643,
257186.036215643,
447646.036215643,
200652.036215643,
510409.036215643,
193.036215643166,
8788626.76409155
),
working_capital = 125000,
remove_cumulative_cols = TRUE,
include_final_day = TRUE,
is_annual_budget = T,
distribution_frequency = NA) {
distribution_frequencies <-
c(NA,
'weekly',
'monthly',
'quarterly',
'yearly',
'sale')
distribution_frequency <-
distribution_frequency %>% str_to_lower() %>%
str_replace_all('yearly', 'annually') %>% str_replace_all('residual', 'sale')
if (!distribution_frequency %>% str_to_lower() %in% distribution_frequencies) {
distribution_frequency <- 'annually'
}
dates <-
dates %>%
ymd()
is_annual_budget <-
as.numeric((dates[2] - dates[1] + 1)) %%
364 == 1
if (distribution_frequency %in% c(NA, 'sale')) {
is_at_sale <-
T
} else {
is_at_sale <-
F
}
## Distribution
distribution_dates_df <-
tibble(date = dates) %>%
mutate(idPeriod = 0:(nrow(.) - 1))
distribution_dates_df <-
distribution_dates_df %>%
mutate(
isDistribution =
case_when(
(
idPeriod > 0 &
is_annual_budget == T &
(!distribution_frequency == 'sale')
) ~ T,
(
is_at_sale == T &
distribution_dates_df$idPeriod == max(distribution_dates_df$idPeriod)
) ~ T,
NA ~ F
))
distribution_dates_df <-
distribution_dates_df %>%
mutate_at('isDistribution',
funs(ifelse(isDistribution %>% is.na(), FALSE, .) %>% as.numeric()))
if (working_capital > 0) {
working_capital <-
-working_capital
}
cf_data <-
tibble(date = dates %>% ymd(),
cashFlow = cash_flows %>% currency(digits = 2)) %>%
mutate(idPeriod = 0:(nrow(.) - 1)) %>%
dplyr::select(idPeriod, everything()) %>%
left_join(distribution_dates_df) %>%
suppressMessages()
cf_data <-
cf_data %>%
mutate(
isSalePeriod = if_else(idPeriod == max(idPeriod), T, F),
workingCapital =
case_when(
cf_data$idPeriod == 0 ~ working_capital,
cf_data$idPeriod == max(cf_data$idPeriod) ~ -working_capital,
TRUE ~ 0
),
totalCF = workingCapital + cashFlow,
capitalContribution = if_else(totalCF < 0, -totalCF, 0),
cashAvailableDistribution = if_else(totalCF > 0, -totalCF, 0),
capitalDistributionCurrent = cashAvailableDistribution * isDistribution,
undistributedCash = capitalDistributionCurrent - cashAvailableDistribution,
cumUndistributedCash = -cumsum(undistributedCash),
distributionAccruedCash = cumUndistributedCash * isDistribution,
capitalDistribution = distributionAccruedCash + capitalDistributionCurrent,
capitalCF = capitalContribution + capitalDistribution
) %>%
dplyr::select(
-c(
undistributedCash,
cumUndistributedCash,
distributionAccruedCash,
capitalDistributionCurrent
)
) %>%
mutate(
daysAccrued = as.numeric((date - dplyr::lag(date))),
daysAccrued = ifelse(daysAccrued %>% is.na(), 0, daysAccrued),
cumDays = cumsum(daysAccrued),
cumContribution = cumsum(capitalContribution),
cumDistribution = cumsum(capitalDistribution),
ratioCapitalReturned = (abs(cumDistribution) / cumContribution) %>% as.numeric,
cumCF = cumsum(cashFlow),
endCash = cumContribution + cumDistribution + cumCF,
beginCash = ifelse(idPeriod == 0, 0, dplyr::lag(endCash))
) %>%
mutate_at(
.vars = c(
"cashFlow",
"workingCapital",
'capitalCF',
"totalCF",
"capitalContribution",
"capitalDistribution",
"beginCash",
"endCash",
"cumContribution",
"cumDistribution",
"cumCF"
),
currency
) %>%
dplyr::select_(
.dots = c(
"idPeriod",
"date",
'daysAccrued',
'cumDays',
"cashFlow",
"isSalePeriod",
"workingCapital",
"totalCF",
"capitalContribution",
"capitalDistribution",
'capitalCF',
'ratioCapitalReturned',
"beginCash",
"endCash",
"cumContribution",
"cumDistribution",
"cumCF"
)
)
if (!(cf_data$totalCF %>% sum) + ((cf_data$capitalContribution %>% sum) + (cf_data$capitalDistribution %>% sum)) == 0) {
stop("Cash does not balance")
}
if (remove_cumulative_cols) {
cf_data <-
cf_data %>%
dplyr::select(-dplyr::matches("cum"))
}
return(cf_data)
}
#' Calculate returns for a given set of cash flows
#'
#' @param dates vector of dates
#' @param cash_flows vector of cash flows
#' @param working_capital amount of working capital, minimum cash
#' @param remove_cumulative_cols remove summary columns
#' @param include_final_day include the final day in calculation
#' @param distribution_frequency when is the cash distributed
#' @param date_format format of the date inputs
#' @param scale_to_100 scale numbers to 100
#' @param return_percentage return percentages
#' @param return_df return data frame
#' @param return_message include a message
#'
#' @return tibble
#' @export
#' @family calculation
#' @family leveraged finance calculation
#' @family partnership calculation
#' @examples
#' calculate_cash_flows_returns(dates = c( "2016-09-01", "2017-08-31", "2018-08-31", "2019-08-31", "2020-08-31", "2021-08-31", "2022-08-31", "2023-08-31" ),
#' cash_flows = c(4151601, -119499.036215643, -257186.036215643, -447646.036215643, -200652.036215643, -510409.036215643, -193.036215643166, -8788626.7640915 ),
#' working_capital = 125000, remove_cumulative_cols = T, distribution_frequency = 'yearly', date_format = '%Y-%m-%d', scale_to_100 = F, return_percentage = F, return_df = T, return_message = T
#' )
calculate_cash_flows_returns <-
function(dates = c(
"2016-09-01",
"2017-08-31",
"2018-08-31",
"2019-08-31",
"2020-08-31",
"2021-08-31",
"2022-08-31",
"2023-08-31"
),
cash_flows = c(
-4151601,
119499.036215643,
257186.036215643,
447646.036215643,
200652.036215643,
510409.036215643,
193.036215643166,
8788626.76409155
),
working_capital = 125000,
remove_cumulative_cols = T,
distribution_frequency = 'annually',
date_format = '%Y-%m-%d',
scale_to_100 = F,
return_percentage = F,
return_df = T,
return_message = T) {
cf_data <-
calculate_cash_flow_dates(
dates = dates,
cash_flows = cash_flows,
working_capital = working_capital,
distribution_frequency = distribution_frequency,
remove_cumulative_cols = remove_cumulative_cols
)
cf_return_data <-
calculate_irr_periods(
dates = cf_data$date,
cash_flows = cf_data$capitalCF,
date_format = date_format,
return_percentage = return_percentage,
return_df = return_df,
return_message = return_message
)
cf_Data
}
scale_to_pct <- function(x) {
if (x > 1) {
x <-
x / 100
}
return(x)
}
.parse_promote_structure <-
function(promote_structure = "20 over a 12") {
hit_words <-
c(" over a ", " over ", "over", "over ", "over a",
"/", " / ", " until ", " on ")
has_no_promote <-
!promote_structure %>%
str_detect(hit_words %>% paste0(collapse = "|"))
if (has_no_promote) {
stop(
"No promote structure detected\nPromote structure looks like 20 over a 12, 30 over 5x, 12 / 9"
)
}
typeHurdle <-
case_when(promote_structure %>% str_detect("x") ~ "multiple",
promote_structure %>% str_detect("until") ~ "amount",
TRUE ~ "pref")
hurdle_promote <-
promote_structure %>%
str_split(pattern = hit_words %>% paste0(collapse = "|")) %>%
future_map(parse_number) %>%
flatten_dbl()
case_when(
typeHurdle == "multiple" ~ hurdle_promote[[1]] %>% scale_to_pct()
)
if (typeHurdle %in% c('multiple', "amount")) {
hurdle_promote[[1]] <-
hurdle_promote[[1]] %>%
scale_to_pct()
} else if (typeHurdle == "pref") {
hurdle_promote <-
hurdle_promote %>%
map_dbl(scale_to_pct)
}
items <-
c("pctPromote", 'valueHurdle')
promote_df <-
tibble(typeHurdle,
item = items,
value = hurdle_promote) %>%
mutate(
item =
case_when(
typeHurdle == "pref" ~ item %>% str_replace(pattern = "valueHurdle", 'pctPref'),
typeHurdle == "multiple" ~ item %>% str_replace(pattern = "valueHurdle", 'ratioCapitalMultiple'),
TRUE ~ item %>% str_replace(pattern = "valueHurdle", 'amountFee')
)
) %>%
spread(item, value)
return(promote_df)
}
#' Tidy waterfall structure
#'
#' This function parses a character vector
#' describing a partnership waterfall into a
#' date frame. The function looks to recogonize
#' whether the promote is based upon an internal rate of return
#' or capital multiple hurdle.
#'
#' @param promote_structures character vector of promote structures
#' @param return_wide return data in wide form
#'
#' @return a data frame
#' @export
#' @importFrom formattable percent
#' @family calculation
#' @family partnership calculation
#' @examples
#' tidy_promote_structure(promote_structures = c("20 over a 12", '30 / 18', "40 over a 10x"), return_wide = T)
tidy_promote_structure <-
function(promote_structures = c("20 over a 12", '30 / 18', "40 over a 10x"),
return_wide = F) {
parse_promote_structure_safe <-
purrr::possibly(.parse_promote_structure, tibble())
promote_data <-
seq_along(promote_structures) %>%
future_map_dfr(function(x) {
parse_promote_structure_safe(promote_structure = promote_structures[x]) %>%
mutate(tierWaterfall = x) %>%
dplyr::select(tierWaterfall, everything())
})
if (return_wide) {
promote_data <-
promote_data %>%
gather(item, hurdle, -c(tierWaterfall)) %>%
replace_na(list(hurdle = 0)) %>%
arrange(tierWaterfall) %>%
unite(item, item, tierWaterfall, sep = '') %>%
mutate(item = item %>% factor(ordered = T, levels = item)) %>%
spread(item, hurdle)
promote_data <-
promote_data %>%
mutate_at(.vars =
promote_data %>% dplyr::select(dplyr::matches("^pct[A-Z]|ratio[A-Z]")) %>% names,
funs(. %>% as.numeric)) %>%
mutate_at(.vars =
promote_data %>% dplyr::select(dplyr::matches("^pct[A-Z]")) %>% names,
funs(. %>% percent))
} else {
promote_data <-
promote_data %>%
mutate(nameTier = promote_structures) %>%
dplyr::select(tierWaterfall, nameTier, everything())
promote_data <-
promote_data %>%
mutate_at(.vars =
promote_data %>% dplyr::select(dplyr::matches("^pct[A-Z]")) %>% names,
funs(. %>% percent)) %>%
dplyr::select(
tierWaterfall,
nameTier,
typeHurdle,
dplyr::matches("pctPref|ratioCapitalMultiple|amountFee"),
pctPromote,
everything()
)
}
return(promote_data)
}
#' Accrued preference
#'
#' This function calcuates accrued
#' preferences for a specified inputs
#'
#' @param pct_pref rate of accrued preference
#' @param is_actual_360 \code{TRUE} calculate rate actual/360 terms
#' @param days count of days
#' @param equity_bb vector of begining equity balance
#' @param pref_accrued_bb vector of accrued preference begining balance
#'
#' @return a \code{data_Fram}
#' @export
#' @import dplyr
#' @importFrom formattable currency
#' @family calculation
#' @family partnership calculation
#' @examples
#' calculate_days_accrued_pref(pct_pref = .1, is_actual_360 = T, days = 31, equity_bb = 1700000.00, pref_accrued_bb = 0)
calculate_days_accrued_pref <-
function(pct_pref = .1,
is_actual_360 = TRUE,
days = 31,
equity_bb = 1700000.00,
pref_accrued_bb = 0) {
if (is_actual_360) {
accrual_days <-
360
} else {
accrual_days <-
365
}
calc_basis <-
(equity_bb + pref_accrued_bb) %>% currency(digits = 2)
accrued_pref <-
((pct_pref / accrual_days) * days * calc_basis)
return(accrued_pref)
}
.get_pct_to_promote <-
function(promote_df, tier_waterfall = 2) {
if (tier_waterfall %in% promote_df$tierWaterfall) {
to_promote <-
promote_df %>%
dplyr::filter(tierWaterfall == tier_waterfall) %>%
.$pctPromote
} else {
to_promote <-
0
}
return(to_promote)
}
.waterfall_tier_df <-
function(tiers = 1:5,
return_wide = F) {
waterfall_df <-
tibble(
tierWaterfall = 1,
bbAccruedPref = 0,
accruedPref = 0,
distributionPriorPref = 0,
toAccruedPref = 0,
ebAccruedPref = 0,
bbCapitalMultiple = 0,
capitalMultipleDraw = 0,
toEquity = 0,
distributionPriorMultiple = 0,
toCapitalMultiple = 0,
ebCapitalMultiple = 0,
bbFee = 0,
feeDraw = 0,
distributionPriorFee = 0,
toFee = 0,
ebFee = 0,
toPromote = 0,
toCapital = 0
)
if (tiers %>% length > 1) {
other_tiers <-
tiers[tiers > 1] %>%
future_map_dfr(function(x) {
tibble(
tierWaterfall = rep(x),
bbAccruedPref = 0,
accruedPref = 0,
distributionPriorPref = 0,
toAccruedPref = 0,
ebAccruedPref = 0,
bbCapitalMultiple = 0,
capitalMultipleDraw = 0,
toEquity = 0,
distributionPriorMultiple = 0,
toCapitalMultiple = 0,
ebCapitalMultiple = 0,
bbFee = 0,
feeDraw = 0,
distributionPriorFee = 0,
toFee = 0,
ebFee = 0,
toPromote = 0,
toCapital = 0
)
})
waterfall_df <-
waterfall_df %>%
bind_rows(other_tiers)
}
convert_cols <- waterfall_df %>% select_if(is.numeric) %>% select(-dplyr::matches("idPeriod|tier")) %>% names()
waterfall_df <-
waterfall_df %>%
mutate_at(convert_cols, currency)
if (return_wide) {
waterfall_df <-
waterfall_df %>%
gather(item, amount, -tierWaterfall, convert = F) %>%
arrange(tierWaterfall, item) %>%
unite(item, item, tierWaterfall, sep = '') %>%
suppressWarnings()
col_order <-
waterfall_df$item
waterfall_df <-
waterfall_df %>%
spread(item, amount) %>%
dplyr::select(one_of(col_order))
}
return(waterfall_df)
}
.get_initial_equity_df <-
function(equity_bb = 0,
to_equity = 0,
period = 0,
equity_draw = 0) {
equityBB <-
equity_bb
toEquity <-
to_equity
equityDraw <-
equity_draw
equityEB <-
equityBB + equityDraw + toEquity
equity_df <-
tibble(idPeriod = period,
equityBB,
equityDraw,
toEquity,
equityEB) %>%
mutate_at(
.vars = c('equityBB', 'equityDraw',
'toEquity', 'equityEB'),
.funs = currency
)
return(equity_df)
}
#' Cash-flow waterfall
#'
#' This function performs waterfall calculations
#' on a set of leveraged or unleveraged cash flows
#' based upon the user's inputs
#'
#'
#' @param dates vector of dates in year-month-day format
#' @param cash_flows vector of cash flows
#' @param working_capital amount of working capital
#' @param promote_structure character vector of promote structures
#' @param distribution_frequency frequency of distributions
#' @param is_actual_360 \code{TRUE} is the rate of return actual 360
#' @param widen_promote_structure \code{TRUE} widen the promote structure
#' @param bind_to_cf \code{TRUE} bind results to data frame
#' @param remove_zero_cols \code{TRUE} remove zero-value columns
#' @param widen_waterfall \code{TRUE} returns waterfall in wide form
#' @import tidyr formattable dplyr stringr
#' @return tibble
#' @export
#' @family calculation
#' @family leveraged finance calculation
#' @family partnership calculation
#' @examples
#' calculate_cash_flow_waterfall(dates = c("2015-03-11", "2015-11-20", "2016-10-15"), cash_flows = c(-100000, -200000, 698906.76849),
#' working_capital = 0, promote_structure = c("20 / 12", "30 / 18"),
#' distribution_frequency = NA, is_actual_360 = TRUE,
#' widen_promote_structure = FALSE, bind_to_cf = FALSE,
#' remove_zero_cols = TRUE, widen_waterfall = FALSE)
calculate_cash_flow_waterfall <-
function(dates =
c("2015-03-11", "2015-11-20", "2016-10-15"),
cash_flows = c(-100000, -200000, 698906.76849),
working_capital = 0,
promote_structure = c("20 / 12", "30 / 18"),
distribution_frequency = NA,
is_actual_360 = TRUE,
widen_promote_structure = FALSE,
bind_to_cf = FALSE,
remove_zero_cols = TRUE,
widen_waterfall = FALSE) {
options(scipen = 999999)
if (class(dates) != "Date") {
dates <-
readr::parse_date(dates)
}
cf_data <-
calculate_cash_flow_dates(
dates = dates,
cash_flows = cash_flows,
remove_cumulative_cols = T,
working_capital = working_capital,
distribution_frequency = distribution_frequency
)
waterfall_data <-
cf_data %>%
dplyr::select(idPeriod:daysAccrued,
capitalContribution,
capitalDistribution,
capitalCF) %>%
mutate(cashDistributionAvailable = -pmin(0, capitalCF) %>% currency())
promote_df <-
tidy_promote_structure(promote_structures = promote_structure,
return_wide = widen_promote_structure)
waterfall_periods <-
waterfall_data$idPeriod
tiers <-
promote_df$tierWaterfall
waterfall_df <-
tibble()
for (x in seq_along(waterfall_periods)) {
period <-
waterfall_periods[x]
period_data <-
waterfall_data %>%
dplyr::filter(idPeriod == period)
days <-
period_data$daysAccrued
equityDraw <-
period_data$capitalContribution
periodCAD <-
period_data$cashDistributionAvailable %>% as.numeric() %>%digits(2) %>% currency
if (period == 0) {
equity_df <-
.get_initial_equity_df(
equity_bb = 0,
to_equity = 0,
equity_draw = equityDraw,
period = 0
)
waterfall_df <-
.waterfall_tier_df(tiers = tiers) %>%
mutate(idPeriod = period) %>%
dplyr::select(idPeriod, everything())
}
if (period > 0) {
equityBB <-
equity_df %>% dplyr::filter(idPeriod == period - 1) %>% .$equityEB
for (tier in tiers) {
typeHurdle <-
promote_df %>% dplyr::filter(tierWaterfall == tier) %>% .$typeHurdle
bbAccruedPref <-
max(
0,
waterfall_df %>% dplyr::filter(idPeriod == period - 1 &
tierWaterfall == tier) %>% .$ebAccruedPref
)
bbCapitalMultiple <-
max(
0,
waterfall_df %>% dplyr::filter(idPeriod == period - 1, tierWaterfall == tier) %>% .$ebCapitalMultiple
)
tierPromote <-
promote_df %>% dplyr::filter(tierWaterfall == tier) %>% .$pctPromote
if (tier == max(tiers)) {
is_max_tier <-
T
} else {
is_max_tier <-
F
}
if (typeHurdle == 'pref') {
tierPref <-
promote_df %>% dplyr::filter(tierWaterfall == tier) %>% .$pctPref
accruedPref <-
calculate_days_accrued_pref(
pct_pref = tierPref,
is_actual_360 = is_actual_360,
days = days,
equity_bb = equityBB,
pref_accrued_bb = bbAccruedPref
)
capitalMultipleDraw <-
0
distributionPriorMultiple <-
0
toCapitalMultiple <-
0
ebCapitalMultiple <-
0
if (tier == 1) {
toAccruedPref <-
-min(periodCAD, (accruedPref + bbAccruedPref)) %>% as.numeric() %>%digits(2)
ebAccruedPref <-
(bbAccruedPref + accruedPref + toAccruedPref) %>% as.numeric() %>%digits(2)
cash_to_equity <-
max(0, (periodCAD + toAccruedPref)) %>% as.numeric() %>%digits(2)
toEquity <-
-min(cash_to_equity, (equityBB + equityDraw)) %>% as.numeric() %>%digits(2)
equityEB <-
(equityBB + equityDraw + toEquity) %>% as.numeric() %>%digits(2)
to_promote_tier <-
max(0, (cash_to_equity + toEquity)) %>% as.numeric() %>%digits(2)
distributionPriorPref <-
0
if (is_max_tier) {
toPromote <-
-to_promote_tier * tierPromote
toCapital <-
-to_promote_tier * (1 - tierPromote)
}
if (is_max_tier == F) {
typeNextTier <-
promote_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$typeHurdle
if (typeNextTier == 'pref') {
nextPref <-
promote_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$pctPref
bbAccruedPrefNext <-
waterfall_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$bbAccruedPref
prefAccruedNext <-
calculate_days_accrued_pref(
pct_pref = nextPref,
is_actual_360 = is_actual_360,
days = days,
equity_bb = equityBB,
pref_accrued_bb = bbAccruedPrefNext
)
cash_for_promote <-
-min(to_promote_tier, max(
0,
(bbAccruedPref + prefAccruedNext + toAccruedPref) / (1 - tierPromote)
))
}
if (typeNextTier == 'multiple') {
nextMultiple <-
promote_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$ratioCapitalMultiple
bbCapitalMultipleNext <-
waterfall_df %>% dplyr::filter(idPeriod == period - 1, tierWaterfall == tier + 1) %>% .$ebCapitalMultiple
capitalMultipleDrawNext <-
(equity_df %>% dplyr::filter(idPeriod == period - 1) %>% .$equityDraw) * nextMultiple
cash_for_promote <-
-min(to_promote_tier, max(
0,
(
bbCapitalMultipleNext + capitalMultipleDrawNext + toAccruedPref
) / (1 - tierPromote)
))
}
toCapital <-
cash_for_promote * (1 - tierPromote)
toPromote <-
cash_for_promote * (tierPromote)
}
}
if (tier > 1) {
priorLevelDistribution <-
waterfall_df %>%
dplyr::filter(idPeriod == period &
tierWaterfall < tier) %>% dplyr::select(toAccruedPref,
toPromote,
toCapital,
toCapitalMultiple) %>% gather(item, value) %>%
.$value %>% sum
equityDistribution <-
equity_df %>% dplyr::filter(idPeriod == period) %>% dplyr::select(toEquity) %>% gather(item, value) %>% .$value %>% sum
remainingCash <-
periodCAD + priorLevelDistribution + equityDistribution
distributionPriorMultiple <-
waterfall_df %>% dplyr::filter(idPeriod == period &
tierWaterfall == (tier - 1)) %>% dplyr::select(distributionPriorMultiple) %>% gather(item, value) %>% .$value %>% sum
distributionPriorPref <-
waterfall_df %>% dplyr::filter(idPeriod == period &
tierWaterfall == (tier - 1)) %>% dplyr::select(distributionPriorPref, toAccruedPref) %>% gather(item, value) %>% .$value %>% sum
toAccruedPref <-
-max(0, min(
remainingCash,
(
bbAccruedPref + accruedPref + distributionPriorPref + distributionPriorMultiple
)
))
to_promote_tier <-
remainingCash + toAccruedPref
ebAccruedPref <-
bbAccruedPref + accruedPref + distributionPriorPref + distributionPriorMultiple + toAccruedPref
if (is_max_tier) {
toPromote <-
-to_promote_tier * tierPromote
toCapital <-
-to_promote_tier * (1 - tierPromote)
}
if (is_max_tier == F) {
typeNextTier <-
promote_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$typeHurdle
if (typeNextTier == 'pref') {
nextPref <-
promote_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$pctPref
bbAccruedPrefNext <-
waterfall_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$bbAccruedPref
prefAccruedNext <-
calculate_days_accrued_pref(
pct_pref = nextPref,
is_actual_360 = is_actual_360,
days = days,
equity_bb = equityBB,
pref_accrued_bb = bbAccruedPrefNext
)
cash_for_promote <-
-min(to_promote_tier, max(
0,
(bbAccruedPref + prefAccruedNext + toAccruedPref) / (1 - tierPromote)
))
}
if (typeNextTier == 'multiple') {
nextMultiple <-
promote_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$ratioCapitalMultiple
bbCapitalMultipleNext <-
waterfall_df %>% dplyr::filter(idPeriod == period - 1, tierWaterfall == tier + 1) %>% .$ebCapitalMultiple
capitalMultipleDrawNext <-
(equity_df %>% dplyr::filter(idPeriod == period - 1) %>% .$equityDraw) * nextMultiple
cash_for_promote <-
-min(to_promote_tier, max(
0,
(
bbCapitalMultipleNext + capitalMultipleDrawNext + toAccruedPref + distributionPriorPref + toEquity
) / (1 - tierPromote)
))
}
toCapital <-
cash_for_promote * (1 - tierPromote)
toPromote <-
cash_for_promote * (tierPromote)
}
}
}
if (typeHurdle == 'multiple') {
ratioCapitalMultiple <-
promote_df %>% dplyr::filter(tierWaterfall == tier) %>% .$ratioCapitalMultiple
if (tier == 1) {
accruedPref <-
0
toAccruedPref <-
0
ebAccruedPref <-
0
cash_to_equity <-
max(0, (periodCAD + toAccruedPref)) %>% as.numeric() %>%digits(2) %>% currency
toEquity <-
-min(cash_to_equity, (equityBB + equityDraw)) %>% as.numeric() %>%digits(2) %>% currency
equityEB <-
(equityBB + equityDraw + toEquity) %>% as.numeric() %>%digits(2) %>% currency
cash_to_multiple <-
max(0, (cash_to_equity + toEquity)) %>% as.numeric() %>%digits(2) %>% currency
capitalMultipleDraw <-
(equity_df %>% dplyr::filter(idPeriod == period - 1) %>% .$equityDraw) * ratioCapitalMultiple
toCapitalMultiple <-
-min(cash_to_multiple,
(bbCapitalMultiple + capitalMultipleDraw + toEquity)) %>% as.numeric() %>%digits(2) %>%
currency
ebCapitalMultiple <-
bbCapitalMultiple + capitalMultipleDraw + toEquity + toCapitalMultiple
to_promote_tier <-
max(0, (cash_to_multiple + toCapitalMultiple)) %>% as.numeric() %>%digits(2) %>%
currency
distributionPriorPref <-
0
distributionPriorMultiple <-
0
if (is_max_tier) {
toPromote <-
-to_promote_tier * tierPromote
toCapital <-
-to_promote_tier * (1 - tierPromote)
}
if (is_max_tier == F) {
nextMultiple <-
promote_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$ratioCapitalMultiple
bbCapitalMultipleNext <-
waterfall_df %>% dplyr::filter(idPeriod == period - 1, tierWaterfall == tier + 1) %>% .$ebCapitalMultiple
capitalMultipleDrawNext <-
(equity_df %>% dplyr::filter(idPeriod == period - 1) %>% .$equityDraw) * nextMultiple
cash_for_promote <-
-min(to_promote_tier, max(
0,
(
bbCapitalMultipleNext + capitalMultipleDrawNext + toCapitalMultiple + toEquity
) / (1 - tierPromote)
))
toCapital <-
cash_for_promote * (1 - tierPromote)
toPromote <-
cash_for_promote * (tierPromote)
}
}
if (tier > 1) {
priorLevelDistribution <-
waterfall_df %>%
dplyr::filter(idPeriod == period &
tierWaterfall < tier) %>%
dplyr::select(tierWaterfall,
toAccruedPref,
toPromote,
toCapital,
toCapitalMultiple) %>%
gather(item, value, -tierWaterfall) %>% .$value %>% sum %>%
currency
equityDistribution <-
equity_df %>% dplyr::filter(idPeriod == period) %>% dplyr::select(toEquity) %>% gather(item, value) %>% .$value %>% sum %>% currency
remainingCash <-
periodCAD + priorLevelDistribution + equityDistribution
accruedPref <-
0
toAccruedPref <-
0
ebAccruedPref <-
bbAccruedPref + accruedPref + toAccruedPref
capitalMultipleDraw <-
(equity_df %>% dplyr::filter(idPeriod == period - 1) %>% .$equityDraw) * ratioCapitalMultiple %>%
currency()
distributionPriorMultiple <-
waterfall_df %>%
dplyr::filter(idPeriod == period &
tierWaterfall <= (tier - 1)) %>%
dplyr::select(toCapitalMultiple, toCapital) %>% gather(item, value) %>%
.$value %>%
sum %>%
currency
distributionPriorPref <-
waterfall_df %>% dplyr::filter(idPeriod == period &
tierWaterfall <= (tier - 1)) %>% dplyr::select(toAccruedPref) %>% gather(item, value) %>% .$value %>% sum %>%
currency
priorEquity <-
waterfall_df %>% dplyr::filter(idPeriod == period &
tierWaterfall == (tier - 1)) %>% dplyr::select(toEquity) %>% gather(item, value) %>% .$value %>% sum %>%
currency()
toCapitalMultiple <-
-max(0,
min(
remainingCash,
(
bbCapitalMultiple + capitalMultipleDraw + distributionPriorMultiple + distributionPriorPref + priorEquity
)
)) %>%
currency()
to_promote_tier <-
remainingCash + toCapitalMultiple
ebCapitalMultiple <-
bbCapitalMultiple + capitalMultipleDraw + distributionPriorMultiple + distributionPriorPref + priorEquity + toCapitalMultiple
if (is_max_tier) {
toPromote <-
-to_promote_tier * tierPromote
toCapital <-
-to_promote_tier * (1 - tierPromote)
}
if (is_max_tier == F) {
nextMultiple <-
promote_df %>%
dplyr::filter(tierWaterfall == tier + 1) %>% .$ratioCapitalMultiple
bbCapitalMultipleNext <-
waterfall_df %>% dplyr::filter(idPeriod == period - 1, tierWaterfall == tier + 1) %>% .$ebCapitalMultiple %>%
currency()
capitalMultipleDrawNext <-
(equity_df %>% dplyr::filter(idPeriod == period - 1) %>% .$equityDraw) * nextMultiple %>%
currency
cash_for_promote <-
-min(to_promote_tier, max(
0,
(
bbCapitalMultipleNext + capitalMultipleDrawNext + toCapitalMultiple + distributionPriorMultiple + distributionPriorPref + equityDistribution
) / (1 - tierPromote)
))
toCapital <-
cash_for_promote * (1 - tierPromote)
toPromote <-
cash_for_promote * (tierPromote)
}
}
}
period_waterfall <-
tibble(
idPeriod = period,
tierWaterfall = tier,
bbAccruedPref,
accruedPref,
distributionPriorPref = distributionPriorPref,
toAccruedPref,
ebAccruedPref,
bbCapitalMultiple,
capitalMultipleDraw,
distributionPriorMultiple,
toEquity,
toCapitalMultiple,
ebCapitalMultiple,
toPromote,
toCapital
) %>%
mutate_if(is.numeric, as.numeric)
waterfall_df <-
waterfall_df %>%
mutate_if(is.numeric, as.numeric) %>%
bind_rows(period_waterfall) %>%
distinct()
period_equity <-
tibble(idPeriod = period,
equityBB,
equityDraw,
toEquity,
equityEB)
equity_df <-
equity_df %>%
bind_rows(period_equity) %>%
distinct()
}
}
}
if ('toEquity' %in% names(waterfall_df)) {
waterfall_df <-
waterfall_df %>% dplyr::select(-toEquity)
}
if (remove_zero_cols) {
waterfall_df <-
waterfall_df %>% dplyr::select(which(colSums(abs(.) != 0) > 0))
}
waterfall_df <-
waterfall_df %>%
mutate_at(waterfall_df %>% dplyr::select(-c(idPeriod, tierWaterfall)) %>% names,
.funs = currency)
equity_df <-
equity_df %>%
mutate_at(equity_df %>% dplyr::select(-c(idPeriod)) %>% names,
.funs = currency)
equityDistributions <-
equity_df$toEquity %>% sum
levelDistributions <-
waterfall_df %>% dplyr::select(dplyr::matches("to")) %>% gather(item, value) %>% .$value %>% sum %>% currency %>% suppressWarnings()
cash_check <-
((cf_data$capitalDistribution %>% sum %>% abs) + (equityDistributions + levelDistributions)
) %>% as.integer()
if (!cash_check == 0) {
stop("Waterfall does not tie to distributable by\n" %>%
paste0(cash_check %>% currency))
}
if (widen_waterfall) {
col_order <-
waterfall_df %>%
gather(item, value, -c(idPeriod, tierWaterfall)) %>%
arrange(idPeriod, tierWaterfall) %>%
unite(item, item, tierWaterfall, sep = '') %>%
.$item %>%
suppressWarnings()
waterfall_df <-
waterfall_df %>%
gather(item, value, -c(idPeriod, tierWaterfall)) %>%
arrange(idPeriod, tierWaterfall) %>%
unite(item, item, tierWaterfall, sep = '') %>%
spread(item, value) %>%
mutate_at(col_order, currency) %>%
dplyr::select(one_of(c('idPeriod', col_order))) %>%
suppressWarnings()
waterfall_df <-
equity_df %>%
left_join(waterfall_df) %>%
suppressMessages()
if (bind_to_cf) {
waterfall_df <-
cf_data %>%
left_join(waterfall_df) %>%
suppressMessages()
}
}
if (widen_waterfall == F) {
waterfall_df <-
waterfall_df %>%
bind_rows(equity_df %>% mutate(tierWaterfall = 0)) %>%
arrange(idPeriod, tierWaterfall) %>%
mutate(tierWaterfall = tierWaterfall + 1)
numeric_cols <-
waterfall_df %>% dplyr::select(-c(idPeriod, tierWaterfall)) %>% names
waterfall_df <-
waterfall_df %>%
mutate_if(is.numeric, as.numeric) %>%
mutate_at(
.vars = numeric_cols,
.funs = function(x)
if_else(x %>% is.na, 0, x)
)
currency_vars <- waterfall_df %>%
select_if(is.numeric) %>%
select(-c(idPeriod, tierWaterfall)) %>%
names()
waterfall_df <-
waterfall_df %>%
mutate_at(currency_vars, formattable::currency)
}
waterfall_df <-
waterfall_df %>%
left_join(waterfall_data %>% dplyr::select(idPeriod, date)) %>%
dplyr::select(idPeriod, date, everything()) %>%
suppressMessages(
)
waterfall_df
}
#' Partnership waterfall and cash flows
#'
#' This function is a variant of the \code{\link{calculate_cash_flow_waterfall}}
#' function that also includes the calculation of the share of cash flow available
#' to each member based upon the specified percentage equity splits and promote allocation
#'
#' @param dates vector of dates in year-month-day format
#' @param cash_flows vector of cash flows
#' @param working_capital amount of working capital
#' @param promote_structure vector of the promote structure
#' @param general_partner_pct ercentage of capital provided by general partner
#' @param gp_promote_share share of promote to general partner
#' @param unnest_data unnest final results
#' @param exclude_partnership_total exclude total columns
#' @param distribution_frequency frequency of distribution
#' @param is_actual_360 \code{TRUE} is the rate of return actual 360
#' @param widen_promote_structure \code{TRUE} widen the promote structure
#' @param bind_to_cf \code{TRUE} bind results to data frame
#' @param remove_zero_cols \code{TRUE} remove zero-value columns
#' @param widen_waterfall \code{TRUE} returns waterfall in wide form
#' @family calculation
#' @family leveraged finance calculation
#' @family partnership calculation
#' @return \code{tibble}O
#' @export
#'
#' @examples
#' calculate_cash_flow_waterfall_partnership(dates =c("2016-09-01", "2017-08-31"), cash_flows = c(-1500000, 105000000),
#' working_capital = 200000, promote_structure = c("20 over 12", "30 over 20", "50 over 3.5x", "100 over 10x"),
#' general_partner_pct = .05, gp_promote_share = 1, unnest_data = F,
#' exclude_partnership_total = F,
#' distribution_frequency = 'annually', is_actual_360 = TRUE,
#' widen_promote_structure = FALSE, bind_to_cf = FALSE, remove_zero_cols = TRU,
#' widen_waterfall = FALSE)
calculate_cash_flow_waterfall_partnership <-
function(dates =
c("2016-09-01",
"2017-08-31"),
cash_flows = c(-1500000,
105000000),
working_capital = 200000,
promote_structure = c("20 over 12", "30 over 20", "50 over 3.5x", "100 over 10x"),
assign_to_environment = TRUE,
general_partner_pct = .05,
gp_promote_share = 1,
unnest_data = FALSE,
exclude_partnership_total = FALSE,
distribution_frequency = NA,
is_actual_360 = TRUE,
widen_promote_structure = FALSE,
bind_to_cf = FALSE,
remove_zero_cols = TRUE,
widen_waterfall = FALSE) {
options(scipen = 9999999)
pct_gp <-
general_partner_pct %>% scale_to_pct()
pct_lp <-
1 - pct_gp
share_gp_promote <-
gp_promote_share
share_lp_promote_share <-
1 - gp_promote_share
promote_name_df <-
tibble(tierWaterfall = 1,
nameTier = "Return of Equity") %>%
bind_rows(
tidy_promote_structure(promote_structures = promote_structure,
return_wide = F) %>%
mutate(tierWaterfall = tierWaterfall + 1) %>%
dplyr::select(tierWaterfall, nameTier)
)
waterfall_data <-
calculate_cash_flow_waterfall(
dates = dates,
cash_flows = cash_flows,
working_capital = working_capital,
is_actual_360 = is_actual_360,
promote_structure = promote_structure,
bind_to_cf = F,
widen_promote_structure = F
) %>%
dplyr::select(idPeriod:tierWaterfall,
equityDraw, toEquity, everything())
entity_waterfall <-
waterfall_data %>%
dplyr::select(idPeriod,
date,
tierWaterfall,
dplyr::matches("equityDraw"),
dplyr::matches("to")) %>%
gather(item, toCF, -c(idPeriod, date, tierWaterfall)) %>%
dplyr::filter(!toCF == 0) %>%
mutate(
toGP = -ifelse(item == 'toPromote', toCF * share_gp_promote, toCF * pct_gp),
toLP = -ifelse(item == 'toPromote',
toCF * share_lp_promote_share,
toCF * pct_lp)
) %>%
mutate_at(.vars = c('toCF', 'toGP', 'toLP'),
.funs = currency) %>%
arrange(idPeriod, tierWaterfall) %>%
left_join(promote_name_df, by = "tierWaterfall") %>%
dplyr::select(idPeriod:tierWaterfall, nameTier, everything())
cash_check <-
((entity_waterfall$toCF %>% sum()) + (entity_waterfall$toGP %>% sum()) + (entity_waterfall$toLP %>% sum())
) %>% as.integer()
if (!cash_check == 0) {
stop("Cash does not tie")
}
entity_cf <-
entity_waterfall %>%
dplyr::select(date, toCF) %>%
group_by(date) %>%
summarise(totalCF = sum(as.numeric(toCF))) %>%
ungroup() %>%
mutate(totalCF = totalCF %>% currency())
gp_cf <-
entity_waterfall %>%
dplyr::select(date, toGP) %>%
group_by(date) %>%
summarise(totalCF = sum(toGP)) %>%
ungroup()
lp_cf <-
entity_waterfall %>%
dplyr::select(date, toLP) %>%
group_by(date) %>%
summarise(totalCF = sum(toLP))
"Partnership returns:" %>% message()
total_return_df <-
calculate_irr_periods(
dates = entity_cf$date,
cash_flows = entity_cf$totalCF,
return_percentage = T,
return_df = T
) %>%
mutate(typeEntity = 'Partnership') %>%
dplyr::select(typeEntity, everything()) %>%
suppressWarnings()
"General Partner returns:" %>% message()
gp_return_df <-
calculate_irr_periods(
dates = gp_cf$date,
cash_flows = -gp_cf$totalCF,
return_percentage = T,
return_df = T
) %>%
mutate(typeEntity = 'General Partner') %>%
dplyr::select(typeEntity, everything())
"Limited Partner returns:" %>% message()
lp_return_df <-
calculate_irr_periods(
dates = lp_cf$date,
cash_flows = -lp_cf$totalCF,
return_percentage = T,
return_df = T
) %>%
mutate(typeEntity = 'Limited Partner') %>%
dplyr::select(typeEntity, everything())
partnership_return_summary <-
gp_return_df %>%
bind_rows(list(lp_return_df, total_return_df))
if (partnership_return_summary %>% tibble::has_name("value")) {
partnership_return_summary <-
partnership_return_summary %>%
spread(metric, value)
}
partnership_return_summary <-
partnership_return_summary %>%
mutate_at(.vars = c('pctIRR'),
.funs = percent) %>%
mutate_at(
.vars = c(
"equityContributions",
"equityDistributions",
"amountProfit"
),
.funs = currency
)
if (exclude_partnership_total) {
partnership_return_summary <-
partnership_return_summary %>%
dplyr::filter(!typeEntity %in% 'Partnership')
}
data <-
tibble(
nameTable =
c(
'Cash Flow Waterfall',
'Entity Waterfall',
'Partnership Return Summary'
),
dataTable =
list(
waterfall_data,
entity_waterfall,
partnership_return_summary
)
)
if (assign_to_environment) {
data <-
data %>%
left_join(tibble(
nameTable = c(
"Cash Flow Waterfall",
"Entity Waterfall",
"Partnership Return Summary"
),
idDF = c(
'cashflowWaterfall',
'entityWaterfall',
'partnershipReturns'
)
)) %>%
suppressMessages()
1:nrow(data) %>%
future_map(function(x) {
table_data <-
data$dataTable[[x]]
df_name <-
data$idDF[[x]]
assign(x = df_name, eval(table_data), envir = .GlobalEnv)
})
data <-
data %>%
dplyr::select(-idDF)
}
if (unnest_data) {
data <-
data %>%
unnest()
}
data
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.