R/monetize.R

Defines functions monetize

Documented in monetize

#' Monetize health impacts

# DESCRIPTION ##################################################################
#' @description
#' This function monetizes health impacts

# ARGUMENTS ####################################################################
#' @param output_attribute \code{List} produced by \code{healthiar::attribute_health()}, \code{healthiar::attribute_lifetable()} or \code{healthiar::compare()} as results.
#' @param impact \code{Numberic value} referring to the health impacts to be monetized (without attribute function). If a \code{Numberic vector} is entered multiple assessments (by year) will be carried out. Be aware that the value for year 0 (current) must be entered, while n_years does not include the year 0. Thus, length of impact = n_years + 1.
#' @param valuation \code{Numberic value} referring to unit value of a health impact.
#' @param discount_rate \code{Numeric value} showing the discount rate for future years.
#' @param discount_shape \code{String} referring to the assumed equation for the discount factor. By default: \code{"exponential"}. Otherwise: \code{"hyperbolic_harvey_1986"} or \code{"hyperbolic_mazur_1987"}.
#' @param n_years \code{Numeric value} referring to number of years in the future to be considered in the discounting and/or inflation. Be aware that the year 0 (without discounting/inflation, i.e. the present) is not be counted here. If a vector is entered in the argument impact, n_years does not need to be entered (length of impact = n_years + 1).
#' @param inflation_rate \code{Numeric value} between 0 and 1 referring to the annual inflation (increase of prices).
#' This value is used to adjust monetization for inflation (converting nominal into real values by appyling a deflator).
#' If this adjustment for inflation is not needed leave this argument empty
#' (default value = NULL).
#' @param real_growth_rate \code{Numeric value} between 0 and 1 referring
#' to the annual real-term appreciation in the societal value of health
#' (e.g., income elasticity).
#' This adjusts the valuation upward to reflect rising wealth,
#' independent of general price inflation.
#' @param info \code{String}, \code{data frame} or \code{tibble} providing \strong{information about the assessment}. Only attached if \code{impact} is entered by the users. If \code{output_attribute} is entered, use \code{info} in that function or add the column manually. \emph{Optional argument.}

# DETAILS ######################################################################
#' @details
#'
#' \strong{Methodology}
#'
#' This function monetize health impacts valuating them and
#' applying discounting \insertCite{Frederick2002_jel}{healthiar}
#' and considering inflation \insertCite{Brealey2023_book}{healthiar}.
#'
#' If the monetized values require adjustment for inflation,
#' a deflator based on \code{inflation_rate} can be applied
#' \insertCite{HMTreasury2022_greenbook}{healthiar}.
#'
#' If the monetized values require adjustment for base valuation upward,
#' a factor based \code{valuation growth} can be applied
#' \insertCite{OECD2012_book}{healthiar}.
#'
#' One of the following three discount shapes can be selected:
#' \itemize{
#'  \item Exponential \insertCite{Frederick2002_jel}{healthiar}
#'  \item Hyperbolic as \insertCite{Harvey1986_ms;textual}{healthiar}
#'  \item Hyperbolic as \insertCite{Mazur1987_book;textual}{healthiar}}
#'
#' Detailed information about the methodology (including equations)
#' is available in the package vignette.
#' More specifically, see chapters:
#' \itemize{
#'  \item \href{https://swisstph.github.io/healthiar/articles/intro_to_healthiar.html#monetization}{Monetization}}
#'
# VALUE ########################################################################
#' @returns
#' This function returns a \code{list} containing:
#'
#' 1) \code{monetization_main} (\code{tibble}) containing the main monetized results;
#' \itemize{
#'  \item \code{monetized_impact} (\code{numeric} column)
#'  \item \code{discount_factor} (\code{numeric} column) calculated based on the entered \code{discount_rate}
#'  \item And many more
#' }
#'
#' 2) \code{monetization_detailed} (\code{list}) containing detailed (and interim) results.
#' \itemize{
#'  \item \code{results_by_year} (\code{tibble})
#'  \item \code{health_raw} (\code{tibble}) containing the monetized results for each for each combination of input uncertainty that were provided to the initial \code{attribute_health()} call
#' }
#'
#' If the argument \code{output_attribute} was specified, then the two results elements are added to the existing output.
#'
# EXAMPLES #####################################################################
#' @examples
#' # Goal: monetize the attributable impacts of an existing healthiar
#' # assessment
#' output_attribute <- attribute_health(
#' erf_shape = "log_linear",
#' rr_central = exdat_pm$relative_risk,
#' rr_increment = 10,
#' exp_central = exdat_pm$mean_concentration,
#' cutoff_central = exdat_pm$cut_off_value,
#' bhd_central = exdat_pm$incidence
#' )
#'
#' results <- monetize(
#'   output_attribute = output_attribute,
#'   discount_shape = "exponential",
#'   discount_rate = 0.03,
#'   n_years = 5,
#'   valuation = 50000 # E.g. EURO
#' )
#'
#' # Attributable COPD cases its monetized impact
#' results$monetization_main |>
#'   dplyr::select(impact, monetized_impact)
#'
#'
#' @seealso
#' \itemize{
#'   \item Upstream: \code{\link{attribute_health}}, \code{\link{attribute_lifetable}}, \code{\link{compare}}
#'   \item Alternative: \code{\link{get_inflation_factor}},
#'     \code{\link{get_discount_factor}}, \code{\link{cba}}
#' }
#'
#'
#' @references
#'
#' \insertAllCited{}
#'
#'
#' @author Alberto Castro & Axel Luyten
#'
#' @export



monetize <- function(output_attribute = NULL,
                     impact = NULL,
                     valuation,
                     discount_rate = NULL,
                     discount_shape = "exponential",
                     n_years = NULL,
                     inflation_rate = NULL,
                     real_growth_rate = NULL,
                     info = NULL) {


  # Store input_args
  input_args <-
    get_input_args(environment = base::environment(),
                   call = match.call())

  # Define variables ####
  # Store variables to increase readability of conditions
  #from healthiar
  using_impact_from_healthiar <-
    !base::is.null(output_attribute) & base::is.null(impact)



  # Using compare() before monetize()
  is_compare <-
    "input_args_scen_1" %in% base::names(output_attribute$health_detailed$input_args)

  if(is_compare){

    approach_comparison <-
      base::unique(output_attribute$health_detailed$input_args$approach_comparison)

  } else {
    approach_comparison <- "no_comparison"
  }


  # is_lifetable only can exist if output_attribute is provided
  # and then it has to be checked of is_lifetable is TRUE or FALSE

  if (!base::is.null(output_attribute)) {

    input_table <- output_attribute[["health_detailed"]][["input_table"]]

    # If after attribute_x(), then input table is a tibble
    if (!is_compare) {
      is_lifetable <- base::unique(input_table$is_lifetable)

    # If after compare(), input table is a list
    } else if (approach_comparison == "delta") {

        # When delta, two input tables (one per scenario)

        is_lifetable <- base::unique(input_table[["input_table_scen_1"]]$is_lifetable)

      } else { # If approach comparison "pif" or if no_comparison

        is_lifetable <- base::unique(input_table$is_lifetable)
      }

  } else {
    is_lifetable <- FALSE
    }



  is_not_lifetable <- ! is_lifetable


  # With and without lifetable
  using_impact_from_healthiar_with_lifetable <-
    using_impact_from_healthiar & is_lifetable
  using_impact_from_healthiar_without_lifetable <-
    using_impact_from_healthiar & is_not_lifetable


  # Impact from user input
  using_impact_from_user <- !using_impact_from_healthiar
  # Pay attention: one is vector (multiple values) and the other value (single value)
  using_impact_vector_from_user <- using_impact_from_user & base::length(impact)>1
  using_impact_value_from_user <- using_impact_from_user & !using_impact_vector_from_user


  # Definition of calculation pathways
  # In the case of no life table and single value, only the monetized value of the last year is taken
  taking_last_discounted_year <-
    using_impact_from_healthiar_without_lifetable | using_impact_value_from_user
  # In the case of life table or multiple impact values entered, results must be summed
  summing_across_years <-
    using_impact_vector_from_user | using_impact_from_healthiar_with_lifetable



  # If a vector is entered in impact
  # The discount years are already defined by the length of the vector
  # Users do not need to enter it.
  if(using_impact_vector_from_user){
    n_years <- base::length(impact)-1
  }

  # Validate input data ####

  ## Error if value lower than 0 ####
  for(var_name in c("valuation", "n_years")){

    if(!base::is.null(base::get(var_name)) &&
       base::get(var_name) < 0){

      stop(base::paste0(var_name, " must be higher than 0."),
           call. = FALSE)
    }

  }

  ## Error if value higher than 1 and lower than 0 ####
  for(var_name in c("discount_rate", "inflation_rate")){

    if(!base::is.null(base::get(var_name)) &&
       (base::get(var_name) < 0 | base::get(var_name) > 1)){

      stop(base::paste0(var_name, " must be higher than 0 and lower than 1."),
           call. = FALSE)
    }

  }



  ## Error if values for both impact and output_attribute are passed ####

  if(!base::is.null(impact) && !base::is.null(output_attribute)){
    stop(base::paste0("Enter a value for impact or for output_attribute but not both."),
         call. = FALSE)
  }

  ## Error if no right category is passed passed ####

  if(!discount_shape %in%
     c("exponential", "hyperbolic_harvey_1986", "hyperbolic_mazur_1987")){

    stop(base::paste0("Please, check spelling. discount_shape must have one of this values: ",
                      "exponential, hyperbolic_harvey_1986, hyperbolic_mazur_1987."),
         call. = FALSE)
  }

  ## Error if different year of analysis in life table approach ####

  # if different year of analysis in scen_1 and scen_2


  if(is_compare){

    # Store values of scenarios
    arg_values_scen_1 <- output_attribute$health_detailed$input_args$input_args_scen_1$value
    arg_values_scen_2 <- output_attribute$health_detailed$input_args$input_args_scen_2$value


    error_if_different_baseline <- function(var){
      # If year of analysis are different
      if(!base::identical(arg_values_scen_1[v], arg_values_scen_2[v])){

        # Error because monetize() aims to monetize health impacts from interventions
        # and health impacts from different years cannot be attributed to the intervention

        stop(
          base::paste0("Please, enter the same ", v ,
                       " in both scenarios of the healthiar function compare. ",
                       "Otherwise, the monetization cannot be attributed to an intervention."),
          call. = FALSE)
      }
    }

    for(v in c("bhd_central", "bhd_lower", "bhd_upper")){

      error_if_different_baseline(var = v)

    }

    if(is_lifetable){

      for(v in c("year_of_analysis")){

        error_if_different_baseline(var = v)

      }
    }
  }



  #### error_if_info_with_incompatible_length ####

  if(! base::is.null(info) &&
     ! base::is.null(impact)){

    if(base::is.data.frame(info)){
      length_info <- base::nrow(info)
    } else if (base::is.vector(info)){
      length_info <- base::length(info)
    }

    if( !length_info == base::length(impact) && !length_info == 1){
      base::stop(
        base::paste0("The info vector or data frame columns must have a length of 1 or the same length as impact."),
        call. = FALSE
      )
    }
  }




  ## Warning if user pass n_years with impact ####

  # Then the value will be ignored and the length of impact will be used as n_years

  if( ! base::is.null(input_args$value$n_years)  &&
     base::length(impact) > 1 &&
     !base::is.null(impact)){
    warning(
      base::paste0("n_years is aimed for output_attribute (excluding life table)",
      " and for impact (excluding vector form).",
      " Therefore n_years is ignored here and the length of the vector impact is used instead."),
      call. = FALSE)
  }

  ## Warning if user pass n_years with impact ####

  # Then the value will be ignored and the length of impact will be used as n_years



  if( ! base::is.null(input_args$value$n_years) &&
     is_lifetable){
    warning(
      base::paste0("n_years is aimed for any output_attribute",
                   " and for impact with single value (no vector).",
                   " Therefore n_years is ignored here and the length life table is used instead."),
      call. = FALSE)
  }




  ## Warning if no value for n_years, but discount_rate####

  # Then discount values are ignored because no discount is happening (by default `n_years = 0`)
  # discount_shape has a default value, so it is never NULL
  if(base::is.null(n_years) &&
     base::any(!base::is.null(discount_rate))&&
     # Exclude life table because the n_years are calculated based on life table
     !is_lifetable){
    warning(
      base::paste0("You entered some value in discount_rate,",
                   " but n_years is 0 (default value).",
                   " Therefore no discount is applied."),
      call. = FALSE)
  }






  # Monetize ####

  ## Create function add_monetized_impact() ###############
  # To be used below

  add_monetized_impact  <-
    function(df,
             valuation,
             discount_rate,
             n_years,
             discount_shape,
             inflation_rate,
             real_growth_rate,
             info = NULL) {

      # Define discount years
      if(base::is.null(n_years)){
        n_years_vector <- 0
      } else {
        n_years_vector <- 0:n_years}

      df_with_input <-
        df |>
        # Add columns for input data in the table
        dplyr::mutate(valuation = valuation,
                      discount_rate = discount_rate,
                      n_years = n_years,
                      discount_shape = discount_shape,
                      inflation_rate = inflation_rate,
                      real_growth_rate = real_growth_rate) |>
        # Add info
        add_info(info = info)


      # Add year
      if(summing_across_years){
        # If lifetable or
        # if impact is inserted as vector to refer to different monetized impacts by year
        # (case of real costs, not applicable for nominal)

        df_by_year <-  df_with_input
        df_by_year$year <-
          base::rep(n_years_vector, len = base::nrow(df_with_input))

      } else if(taking_last_discounted_year){
        df_by_year <-
          # Split by year
          dplyr::cross_join(x = tibble::tibble(year = n_years_vector),
                            y = df_with_input)
      }


      df_by_year <-
        df_by_year |>
        dplyr::mutate(
          # 1. Discount: Apply time preference
          discount_factor = get_discount_factor(
            discount_rate = if(base::is.null(discount_rate)) 0 else discount_rate,
            n_years = year,
            discount_shape = discount_shape
          ),


          # 2. Adjust for inflation (deflate): Convert nominal values back to real
          # We only apply the deflator if the user wants a "Real" present value
          # i.e. if the user entered a value in inflation_rate
          deflator_factor = if(!base::is.null(inflation_rate)) {
            get_inflation_factor(n_years = year,
                                 inflation_rate = inflation_rate,
                                 is_deflation = TRUE)
          } else {
            1
          },

          # 3. Adjust for real_growth:
          real_growth_factor = if(!base::is.null(real_growth_rate)) {
            get_inflation_factor(n_years = year,
                                 inflation_rate = real_growth_rate,
                                 is_deflation = FALSE)
          } else {
            1
          },

          # 4. Final Calculation
          monetized_impact = impact * valuation * discount_factor * deflator_factor * real_growth_factor,

          monetized_impact_unadjusted = impact * valuation,
          .after = impact
        )



      # If taking last discounted year ####
      if(taking_last_discounted_year){
        df_relevant <-
          df_by_year|>
          # Keep only the last year
          dplyr::filter(year == base::max(year)) |>
          # Remove the variable discount year because it is not anymore relevant
          # (not by-year results)
          dplyr::select(-year)

        # If summing across discounted years ####
      }else if(summing_across_years){

        grouping_variables <-
          df_by_year |>
          dplyr::select(-dplyr::any_of(c("year")),
                        -dplyr::contains("_factor"),
                        -dplyr::contains("impact")) |>
          base::names()

        df_relevant <-
          df_by_year |>
          dplyr::summarize(
            .by = dplyr::any_of(grouping_variables),
            dplyr::across(dplyr::contains("impact"), sum)
          )
      }

      monetization_main <-
        df_relevant |>
        # Round monetized impacts
        dplyr::mutate(
          monetized_impact_rounded = base::round(monetized_impact),
          .after = monetized_impact)

      ##### Output ####
      monetization <-
        base::list(
          monetization_main = monetization_main,
          monetization_detailed = base::list(results_by_year = df_by_year)
        )

      return(monetization)

    }


  #* IF OUTPUT of attribute ####

  if(using_impact_from_healthiar){

    ##** IF LIFE TABLE method for the health assessment #######

    # If life table.
    if(is_lifetable){

      health_outcome <-
        output_attribute[["health_detailed"]][["input_args"]][["value"]]$health_outcome

      # Store the original data (they refer to health)
      output_health <- output_attribute

      # Obtain n_years
      # Ignore user defined n_years
      # Here the difference between year of analysis and
      # last year of mortality data is to be used
      impact_detailed <- output_health[["health_detailed"]][["results_by_year"]] |>
        dplyr::mutate(
        # Convert year to numeric
        year = base::as.numeric(year))

      # Extract year of analysis
      # If monetizing after compare(), then take year_of_analysis_scen_1
      # year_of_analysis must be the same in scen_1 and scen_2 to be able to monetize
      # See validation above

      if(approach_comparison == "delta"){

          year_of_analysis <- base::unique(impact_detailed$year_of_analysis_scen_1)

        #If monetizing after attribute, then just take the value
      } else { #If pif or no_comparison
        year_of_analysis <- base::unique(impact_detailed$year_of_analysis)
      }


      n_years <- base::max(impact_detailed$year) - year_of_analysis


      # Output will be adapted according to monetized impacts
      impact_detailed <-
        impact_detailed |>
        ## Calculate total, discounted life years (single value) per sex & ci
        dplyr::mutate(
          n_years = n_years,
          discount_rate = discount_rate,
          discount_shape = discount_shape,
          inflation_rate = inflation_rate,
          real_growth_rate = real_growth_rate)

      impact_detailed  <-
        add_monetized_impact(
          df = impact_detailed,
          discount_rate = discount_rate,
          n_years = n_years,
          discount_shape = discount_shape,
          inflation_rate = inflation_rate,
          valuation = valuation,
          real_growth_rate = real_growth_rate)[["monetization_main"]]


      impact_detailed <- impact_detailed |>
        # Round results
        dplyr::mutate(
          # Round impacts and monetized impacts
          impact_rounded = base::round(impact),
          monetized_impact_rounded = base::round(monetized_impact))


      # Calculate impact per 100K inhab.

      if("population" %in% base::colnames(impact_detailed)){
        impact_detailed <-
          impact_detailed |>
          dplyr::mutate(
            impact_per_100k_inhab = (impact / population) *1E5
          )
      }

      # Get the main and detailed output by aggregating and/or filtering cases (rows)
      output_monetization <-
        get_output(results_raw = impact_detailed) |>
        # Rename the list elements (not anymore health but health including monetization)
        stats::setNames(c("monetization_main", "monetization_detailed"))

      # Keep only the main detailed data frame (raw) for monetization
      output_monetization[["monetization_detailed"]] <-
        output_monetization[["monetization_detailed"]][["results_raw"]]

      # Add the list elements health_main and health_detailed
      output_monetization <-
        c(output_health,
          output_monetization)

    }else if (is_not_lifetable){

      ##** IF WITHOUT LIFE TABLE #######

      # Monetize impacts using health main
      output_monetization_health_main <-
        add_monetized_impact(df = output_attribute[["health_main"]],
                             valuation = valuation,
                             discount_rate = discount_rate,
                             n_years = n_years,
                             discount_shape = discount_shape,
                             inflation_rate = inflation_rate,
                             real_growth_rate = real_growth_rate)

      # Put together health and monetization output
      output_monetization <-
        c(output_attribute, output_monetization_health_main)


      #Detailed results showing all the details of the health results
      output_monetization[["monetization_detailed"]][["health_raw"]]<-
        add_monetized_impact(df = output_attribute[["health_detailed"]][["results_raw"]],
                             valuation = valuation,
                             discount_rate = discount_rate,
                             n_years = n_years,
                             discount_shape = discount_shape,
                             inflation_rate = inflation_rate,
                             real_growth_rate = real_growth_rate)[["monetization_main"]]
    }


    # For both with and without life table
    # Identify the relevant columns for monetization that are in the output
    relevant_columns <-
      c("info", "geo_id_micro", "geo_id_macro",
        "impact",
        "discount_rate", "discount_shape", "inflation_rate", "n_years",
        "valuation",
        base::paste0("monetized_impact", c("", "_unadjusted", "_rounded")))

    # Keep only relevant columns for monetization
    output_monetization[["monetization_main"]] <-
      output_monetization[["monetization_main"]] |>
      dplyr::select(
        # The columns containing "_ci" are the uncertainties that define the rows
        dplyr::contains("_ci"),
        # Use any_of() instead of all_of() because depending on the calculation pathway
        # there might not be any of the relevant_columns
        dplyr::any_of(relevant_columns))


    #* IF USER INPUT ####

    # If the user only provide a number of the impact (not based on output of attribute)
    # No life table approach when user is entering the health impacts
    # because we cannot access the life table calculation to discount by year
    }else if(using_impact_from_user){

      output_monetization <-
        add_monetized_impact(
          df = tibble::tibble(impact = impact),
          valuation = valuation,
          discount_rate = discount_rate,
          n_years = n_years,
          discount_shape = discount_shape,
          inflation_rate = inflation_rate,
          real_growth_rate = real_growth_rate,
          info = info)

  }


  return(output_monetization)

}

Try the healthiar package in your browser

Any scripts or data that you put into this service are public.

healthiar documentation built on March 12, 2026, 5:07 p.m.