# ── Reproducibility check ─────────────────────────────────────────────────
cat("Session started:", format(Sys.time(), "%Y-%m-%d %H:%M"), "\n")
## Session started: 2026-05-20 15:59
cat("Working directory:", getwd(), "\n")
## Working directory: C:/Users/User/OneDrive - Alma Mater Studiorum Università di Bologna/Documents/Uni/master/hug/survival model clarification
cat("R version:", R.version$version.string, "\n")
## R version: R version 4.4.2 (2024-10-31 ucrt)
##required <- c("tidyverse", "janitor", "knitr", "kableExtra", "scales", "patchwork")
##to_install <- required[!required %in% rownames(installed.packages())]
##if (length(to_install) > 0) install.packages(to_install)
library(tidyverse)
library(janitor)
library(knitr)
library(kableExtra)
library(scales)
library(patchwork)
library(reader)
library(servr)
library(jsonlite)

1 Data loading

1.1 File paths

DATA_DIR <- r"(C:\Users\User\OneDrive - Alma Mater Studiorum Università di Bologna\Documents\Uni\master\hug\survival model clarification)"

path_users  <- file.path(DATA_DIR, "Users.csv")
path_strong <- file.path(DATA_DIR, "Strongaging.csv")
path_weak   <- file.path(DATA_DIR, "Weakaging.csv")

stopifnot(
  "Users.csv not found"       = file.exists(path_users),
  "Strongaging.csv not found" = file.exists(path_strong),
  "Weakaging.csv not found"   = file.exists(path_weak)
)
cat("All three files found.\n")
## All three files found.

1.2 Load raw files

nor_locale <- locale(
  decimal_mark  = ",",
  grouping_mark = ".",
  encoding      = "UTF-8"
)

users_raw  <- read_delim(path_users,  delim = ";", locale = nor_locale,
                         name_repair = "minimal", show_col_types = FALSE)
strong_raw <- read_delim(path_strong, delim = ";", locale = nor_locale,
                         name_repair = "minimal", show_col_types = FALSE)
weak_raw   <- read_delim(path_weak,   delim = ";", locale = nor_locale,
                         name_repair = "minimal", show_col_types = FALSE)

cat("users_raw: ",  nrow(users_raw),  "rows x", ncol(users_raw),  "cols\n")
## users_raw:  360 rows x 7 cols
cat("strong_raw:", nrow(strong_raw), "rows x", ncol(strong_raw), "cols\n")
## strong_raw: 357 rows x 342 cols
cat("weak_raw:  ", nrow(weak_raw),   "rows x", ncol(weak_raw),   "cols\n")
## weak_raw:   357 rows x 342 cols
path_beds     <- file.path(DATA_DIR, "Beds.csv")
path_services <- file.path(DATA_DIR, "Services.csv")

beds_raw     <- read_delim(path_beds,     delim = ";", locale = nor_locale,
                           name_repair = "minimal", show_col_types = FALSE)
services_raw <- read_delim(path_services, delim = ";", locale = nor_locale,
                           name_repair = "minimal", show_col_types = FALSE)

cat("beds_raw:     ", nrow(beds_raw),     "rows x", ncol(beds_raw),     "cols\n")
## beds_raw:      360 rows x 13 cols
cat("services_raw: ", nrow(services_raw), "rows x", ncol(services_raw), "cols\n")
## services_raw:  358 rows x 28 cols
# Show column names so we can write the cleaning chunk
cat("\nbeds_raw columns:\n");     print(names(beds_raw))
## 
## beds_raw columns:
##  [1] "region"                                          
##  [2] "2025 Institution - all available beds (number)"  
##  [3] "2025 Institutions - private beds (number)"       
##  [4] "2025 Institution - municipal beds (number)"      
##  [5] "2025 Nursing home - available beds (number)"     
##  [6] "2025 Nursing home - private beds (number)"       
##  [7] "2025 Nursing home - municipal beds (number)"     
##  [8] "2025 Old peoples' home - available beds (number)"
##  [9] "2025 Beds in relief institutions (number)"       
## [10] "2025 Beds in dwellings for children (number)"    
## [11] "2025 Institutions - beds for dementia (number)"  
## [12] "2025 Institution - rehab beds (number)"          
## [13] "2025 Institution - short-time stay (number)"
cat("\nservices_raw columns:\n"); print(names(services_raw))
## 
## services_raw columns:
##  [1] "contents"                                                                              
##  [2] "region"                                                                                
##  [3] "2025 Practical assistance: daily activities"                                           
##  [4] "2025 Practical assistance: training in daily activities"                               
##  [5] "2025 Practical assistance: personal support contact"                                   
##  [6] "2025 Respite care - outside institutions"                                              
##  [7] "2025 Care benefits"                                                                    
##  [8] "2025 Home nursing"                                                                     
##  [9] "2025 Re-/habilitation outside institutions"                                            
## [10] "2025 Respite care - in institutions repeated"                                          
## [11] "2025 Respite care - in institutions not repeated"                                      
## [12] "2025 Daytime stay in institutions"                                                     
## [13] "2025 Time-limited stay in institutions - health examination/treatment"                 
## [14] "2025 Time-limited stay in institutions - habilitation/rehabilitation"                  
## [15] "2025 Time-limited stay in institutions - other"                                        
## [16] "2025 Long-term stay in institutions"                                                   
## [17] "2025 Night-time stay in institutions"                                                  
## [18] "2025 Municipal emergency help - admission over night"                                  
## [19] "2025 Daytime activity programmes"                                                      
## [20] "2025 Meals-on-wheels"                                                                  
## [21] "2025 Dedicated support"                                                                
## [22] "2025 Dwellings for the aged and disabled, approved by the Norwegian state housing bank"
## [23] "2025 Other dwellings for the aged and disabled administered by the municipalities"     
## [24] "2025 Municipal housing for health care purposes"                                       
## [25] "2025 Electronic medication support"                                                    
## [26] "2025 Alarm call and localiasation technology"                                          
## [27] "2025 Automatic warning aid"                                                            
## [28] "2025 Digital observation"

2 Data cleaning

2.1 Municipality code lookup

We extract the 4-digit SSB municipality code from the region string and work only with that code for all joins. This sidesteps the encoding problem entirely because the code is always four plain ASCII digits regardless of how the name looks.

# The projection files have "contents" in col 1 and "region" in col 2.
# iconv with sub="" drops any byte that cannot be transliterated safely.

muni_lookup <- strong_raw |>
  select(raw = 2) |>
  distinct() |>
  mutate(
    muni_code = as.integer(str_extract(raw, "^\\d{4}")),
    muni_name = iconv(
      str_remove(raw, "^\\d{4}\\s*"),
      to  = "ASCII//TRANSLIT",
      sub = ""
    )
  ) |>
  filter(!is.na(muni_code)) |>
  select(muni_code, muni_name)

cat("Lookup table:", nrow(muni_lookup), "municipalities\n")
## Lookup table: 357 municipalities
head(muni_lookup, 10)

2.2 Users dataset

users <- users_raw |>
  clean_names() |>
  mutate(muni_code = as.integer(str_extract(region, "^\\d{4}"))) |>
  rename(
    nh_residents     = 2,
    dwelling_res     = 3,
    home_care_total  = 4,
    home_care_u66    = 5,
    nh_beds          = 6,
    old_peoples_beds = 7
  ) |>
  mutate(
    across(c(nh_residents, dwelling_res, home_care_total,
             home_care_u66, nh_beds, old_peoples_beds),
           ~ suppressWarnings(as.numeric(.x))),
    total_beds        = nh_beds + replace_na(old_peoples_beds, 0),
    home_care_elderly = home_care_total - replace_na(home_care_u66, 0),
    occupancy_rate    = nh_residents / total_beds
  ) |>
  filter(!is.na(muni_code)) |>
  select(muni_code, nh_residents, dwelling_res, home_care_total,
         home_care_u66, nh_beds, old_peoples_beds,
         total_beds, home_care_elderly, occupancy_rate) |>
  left_join(muni_lookup, by = "muni_code")

glimpse(users)
## Rows: 358
## Columns: 11
## $ muni_code         <int> 3101, 3103, 3105, 3107, 3110, 3112, 3114, 3116, 3118…
## $ nh_residents      <dbl> 212, 275, 362, 585, 38, 52, 21, 28, 318, 38, 31, 12,…
## $ dwelling_res      <dbl> 193, 365, 425, 261, 29, 34, 26, 15, 260, 74, 7, 4, 6…
## $ home_care_total   <dbl> 1081, 2368, 2403, 2936, 198, 345, 406, 215, 1902, 57…
## $ home_care_u66     <dbl> 437, 1127, 1318, 1337, 93, 186, 284, 125, 956, 393, …
## $ nh_beds           <dbl> 211, 268, 385, 600, 39, 50, 26, 41, 324, 54, 43, 17,…
## $ old_peoples_beds  <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ total_beds        <dbl> 211, 268, 385, 600, 39, 50, 26, 41, 324, 54, 43, 17,…
## $ home_care_elderly <dbl> 644, 1241, 1085, 1599, 105, 159, 122, 90, 946, 183, …
## $ occupancy_rate    <dbl> 1.0047393, 1.0261194, 0.9402597, 0.9750000, 0.974359…
## $ muni_name         <chr> "Halden", "Moss", "Sarpsborg", "Fredrikstad", "Hvale…
# ── Clean beds_raw ──────────────────────────────────────────────────────────
beds <- beds_raw |>
  mutate(muni_code = as.integer(str_extract(region, "^\\d{4}"))) |>
  filter(!is.na(muni_code)) |>
  select(-region) |>
  rename_with(
    # Strip the leading "2025 " and trailing " (number)" or " (beds)"
    ~ str_remove(.x, "^\\d{4}\\s+") |>
      str_remove("\\s*\\(number\\)$") |>
      str_remove("\\s*\\(beds\\)$") |>
      str_squish() |>
      janitor::make_clean_names(),
    -muni_code
  ) |>
  mutate(across(-muni_code, ~ suppressWarnings(as.numeric(.x))))

cat("beds columns:\n")
## beds columns:
print(names(beds))
##  [1] "institution_all_available_beds"  "institutions_private_beds"      
##  [3] "institution_municipal_beds"      "nursing_home_available_beds"    
##  [5] "nursing_home_private_beds"       "nursing_home_municipal_beds"    
##  [7] "old_peoples_home_available_beds" "beds_in_relief_institutions"    
##  [9] "beds_in_dwellings_for_children"  "institutions_beds_for_dementia" 
## [11] "institution_rehab_beds"          "institution_short_time_stay"    
## [13] "muni_code"
# ── Clean services_raw ──────────────────────────────────────────────────────
services <- services_raw |>
  select(-1) |>                    # drop the redundant "contents" column
  mutate(muni_code = as.integer(str_extract(region, "^\\d{4}"))) |>
  filter(!is.na(muni_code)) |>
  select(-region) |>
  rename_with(
    ~ str_remove(.x, "^\\d{4}\\s+") |>
      str_remove("\\s*\\(number\\)$") |>
      str_squish() |>
      janitor::make_clean_names(),
    -muni_code
  ) |>
  mutate(across(-muni_code, ~ suppressWarnings(as.numeric(.x))))

cat("\nservices columns:\n")
## 
## services columns:
print(names(services))
##  [1] "practical_assistance_daily_activities"                                           
##  [2] "practical_assistance_training_in_daily_activities"                               
##  [3] "practical_assistance_personal_support_contact"                                   
##  [4] "respite_care_outside_institutions"                                               
##  [5] "care_benefits"                                                                   
##  [6] "home_nursing"                                                                    
##  [7] "re_habilitation_outside_institutions"                                            
##  [8] "respite_care_in_institutions_repeated"                                           
##  [9] "respite_care_in_institutions_not_repeated"                                       
## [10] "daytime_stay_in_institutions"                                                    
## [11] "time_limited_stay_in_institutions_health_examination_treatment"                  
## [12] "time_limited_stay_in_institutions_habilitation_rehabilitation"                   
## [13] "time_limited_stay_in_institutions_other"                                         
## [14] "long_term_stay_in_institutions"                                                  
## [15] "night_time_stay_in_institutions"                                                 
## [16] "municipal_emergency_help_admission_over_night"                                   
## [17] "daytime_activity_programmes"                                                     
## [18] "meals_on_wheels"                                                                 
## [19] "dedicated_support"                                                               
## [20] "dwellings_for_the_aged_and_disabled_approved_by_the_norwegian_state_housing_bank"
## [21] "other_dwellings_for_the_aged_and_disabled_administered_by_the_municipalities"    
## [22] "municipal_housing_for_health_care_purposes"                                      
## [23] "electronic_medication_support"                                                   
## [24] "alarm_call_and_localiasation_technology"                                         
## [25] "automatic_warning_aid"                                                           
## [26] "digital_observation"                                                             
## [27] "muni_code"
# ── Join everything onto users ───────────────────────────────────────────────
users <- users |>
  left_join(beds,     by = "muni_code") |>
  left_join(services, by = "muni_code") |>
  mutate(
    # True total institutional capacity:
    # permanent beds + state-approved dwellings + municipal dwellings
    total_capacity = replace_na(institution_all_available_beds, 0) +
                     replace_na(dwellings_for_the_aged_and_disabled_approved_by_the_norwegian_state_housing_bank, 0) +
                     replace_na(other_dwellings_for_the_aged_and_disabled_administered_by_the_municipalities, 0),

    # True permanent institutional demand (long-term only, no turnover noise)
    permanent_demand = replace_na(long_term_stay_in_institutions, 0) +
                       replace_na(dwellings_for_the_aged_and_disabled_approved_by_the_norwegian_state_housing_bank, 0) +
                       replace_na(other_dwellings_for_the_aged_and_disabled_administered_by_the_municipalities, 0),

    # Clean occupancy rate using permanent demand and true capacity
    true_occupancy = permanent_demand / total_capacity,

    # Dementia-specific bed supply
    dementia_beds = replace_na(institutions_beds_for_dementia, 0)
  )

glimpse(users)
## Rows: 358
## Columns: 53
## $ muni_code                                                                        <int> …
## $ nh_residents                                                                     <dbl> …
## $ dwelling_res                                                                     <dbl> …
## $ home_care_total                                                                  <dbl> …
## $ home_care_u66                                                                    <dbl> …
## $ nh_beds                                                                          <dbl> …
## $ old_peoples_beds                                                                 <dbl> …
## $ total_beds                                                                       <dbl> …
## $ home_care_elderly                                                                <dbl> …
## $ occupancy_rate                                                                   <dbl> …
## $ muni_name                                                                        <chr> …
## $ institution_all_available_beds                                                   <dbl> …
## $ institutions_private_beds                                                        <dbl> …
## $ institution_municipal_beds                                                       <dbl> …
## $ nursing_home_available_beds                                                      <dbl> …
## $ nursing_home_private_beds                                                        <dbl> …
## $ nursing_home_municipal_beds                                                      <dbl> …
## $ old_peoples_home_available_beds                                                  <dbl> …
## $ beds_in_relief_institutions                                                      <dbl> …
## $ beds_in_dwellings_for_children                                                   <dbl> …
## $ institutions_beds_for_dementia                                                   <dbl> …
## $ institution_rehab_beds                                                           <dbl> …
## $ institution_short_time_stay                                                      <dbl> …
## $ practical_assistance_daily_activities                                            <dbl> …
## $ practical_assistance_training_in_daily_activities                                <dbl> …
## $ practical_assistance_personal_support_contact                                    <dbl> …
## $ respite_care_outside_institutions                                                <dbl> …
## $ care_benefits                                                                    <dbl> …
## $ home_nursing                                                                     <dbl> …
## $ re_habilitation_outside_institutions                                             <dbl> …
## $ respite_care_in_institutions_repeated                                            <dbl> …
## $ respite_care_in_institutions_not_repeated                                        <dbl> …
## $ daytime_stay_in_institutions                                                     <dbl> …
## $ time_limited_stay_in_institutions_health_examination_treatment                   <dbl> …
## $ time_limited_stay_in_institutions_habilitation_rehabilitation                    <dbl> …
## $ time_limited_stay_in_institutions_other                                          <dbl> …
## $ long_term_stay_in_institutions                                                   <dbl> …
## $ night_time_stay_in_institutions                                                  <dbl> …
## $ municipal_emergency_help_admission_over_night                                    <dbl> …
## $ daytime_activity_programmes                                                      <dbl> …
## $ meals_on_wheels                                                                  <dbl> …
## $ dedicated_support                                                                <dbl> …
## $ dwellings_for_the_aged_and_disabled_approved_by_the_norwegian_state_housing_bank <dbl> …
## $ other_dwellings_for_the_aged_and_disabled_administered_by_the_municipalities     <dbl> …
## $ municipal_housing_for_health_care_purposes                                       <dbl> …
## $ electronic_medication_support                                                    <dbl> …
## $ alarm_call_and_localiasation_technology                                          <dbl> …
## $ automatic_warning_aid                                                            <dbl> …
## $ digital_observation                                                              <dbl> …
## $ total_capacity                                                                   <dbl> …
## $ permanent_demand                                                                 <dbl> …
## $ true_occupancy                                                                   <dbl> …
## $ dementia_beds                                                                    <dbl> …
users |>
  summarise(
    n_municipalities        = n(),
    total_nh_residents      = sum(nh_residents,       na.rm = TRUE),
    total_nh_beds           = sum(nh_beds,            na.rm = TRUE),
    total_beds_incl_op      = sum(total_beds,         na.rm = TRUE),
    total_home_care_elderly = sum(home_care_elderly,  na.rm = TRUE),
    national_occupancy_rate = total_nh_residents / total_nh_beds,
    n_over_capacity         = sum(occupancy_rate > 1, na.rm = TRUE)
  ) |>
  pivot_longer(everything(), names_to = "Metric", values_to = "Value") |>
  kable(caption = "Users dataset - national totals (cross-check against KOSTRA 2025)",
        digits = 3, format.args = list(big.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Users dataset - national totals (cross-check against KOSTRA 2025)
Metric Value
n_municipalities 358.000
total_nh_residents 37,102.000
total_nh_beds 39,643.600
total_beds_incl_op 39,848.600
total_home_care_elderly 113,343.000
national_occupancy_rate 0.936
n_over_capacity 39.000

2.3 Population projection datasets

# Pivots wide projection file into long format.
# Input columns: contents | region | "2025 67 years" | "2025 68 years" | ...
# Output columns: muni_code | age | year | population | scenario

pivot_to_long <- function(df, scenario_label) {
  df |>
    select(-1) |>
    rename(raw_region = 1) |>
    mutate(muni_code = as.integer(str_extract(raw_region, "^\\d{4}"))) |>
    filter(!is.na(muni_code)) |>
    select(-raw_region) |>
    pivot_longer(
      cols      = -muni_code,
      names_to  = "year_age",
      values_to = "population"
    ) |>
    mutate(
      year       = as.integer(str_extract(year_age, "^\\d{4}")),
      age        = as.integer(str_extract(year_age, "(?<=\\s)\\d+(?=\\s)")),
      population = suppressWarnings(as.numeric(population)),
      scenario   = scenario_label
    ) |>
    filter(!is.na(population), !is.na(age), !is.na(year)) |>
    select(muni_code, age, year, population, scenario)
}
strong_long <- pivot_to_long(strong_raw, "Strong ageing")
weak_long   <- pivot_to_long(weak_raw,   "Weak ageing")
projections <- bind_rows(strong_long, weak_long)

cat("Total projection rows:", nrow(projections), "\n")
## Total projection rows: 242760
cat("Years covered:", paste(range(projections$year), collapse = " to "), "\n")
## Years covered: 2025 to 2034
cat("Age range:   ", paste(range(projections$age),  collapse = " to "), "\n")
## Age range:    67 to 100
cat("Scenarios:   ", paste(unique(projections$scenario), collapse = ", "), "\n")
## Scenarios:    Strong ageing, Weak ageing
projections_banded <- projections |>
  mutate(
    age_band = case_when(
      age >= 67 & age <= 79 ~ "67-79",
      age >= 80 & age <= 89 ~ "80-89",
      age >= 90             ~ "90+",
      TRUE                  ~ NA_character_
    )
  ) |>
  filter(!is.na(age_band)) |>
  group_by(muni_code, scenario, year, age_band) |>
  summarise(population = sum(population, na.rm = TRUE), .groups = "drop")

national_proj <- projections_banded |>
  group_by(scenario, year, age_band) |>
  summarise(population = sum(population, na.rm = TRUE), .groups = "drop")

cat("projections_banded:", nrow(projections_banded), "rows\n")
## projections_banded: 21420 rows
cat("national_proj:     ", nrow(national_proj),      "rows\n")
## national_proj:      60 rows

3 Sanity checks

users |>
  filter(occupancy_rate > 0.95, !is.na(occupancy_rate)) |>
  arrange(desc(occupancy_rate)) |>
  mutate(occupancy_rate = percent(occupancy_rate, accuracy = 0.1)) |>
  select(muni_code, muni_name, nh_residents, nh_beds, total_beds, occupancy_rate) |>
  kable(caption = "Municipalities at or above 95% NH occupancy (2025)",
        format.args = list(big.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Municipalities at or above 95% NH occupancy (2025)
muni_code muni_name nh_residents nh_beds total_beds occupancy_rate
4,202 Grimstad 250 134.0 134.0 186.6%
1,145 Bokn 14 9.0 9.0 155.6%
1,144 Kvits?y 16 11.0 11.0 145.5%
5,536 Lyngen - Ivgu - Yyke? 31 24.0 24.0 129.2%
1,554 Aver?y 54 48.0 48.0 112.5%
5,632 B?tsfjord 18 16.0 16.0 112.5%
1,124 Sola 195 176.0 176.0 110.8%
301 Oslo - Oslove 4,400 3,974.0 3,982.0 110.5%
3,435 V?g? 36 33.0 33.0 109.1%
5,544 Nordreisa - R?isa - Raisi 54 50.0 50.0 108.0%
5,620 Nordkapp 30 28.0 28.0 107.1%
3,437 Sel 66 62.0 62.0 106.5%
4,614 Stord 116 109.0 109.0 106.4%
4,001 Porsgrunn 301 283.0 283.0 106.4%
3,336 Rollag 17 16.0 16.0 106.2%
5,514 Ibestad 18 17.0 17.0 105.9%
5,033 Tydal 19 18.0 18.0 105.6%
1,839 Beiarn 19 18.0 18.0 105.6%
5,053 Inder?y 25 24.0 24.0 104.2%
3,205 Lillestr?m 453 435.0 435.0 104.1%
3,112 R?de 52 50.0 50.0 104.0%
3,433 Skj?k 26 25.0 25.0 104.0%
1,820 Alstahaug 52 50.0 50.0 104.0%
4,651 Stryn 58 56.0 56.0 103.6%
3,301 Drammen 582 563.0 563.0 103.4%
5,605 S?r-Varanger 72 70.0 70.0 102.9%
1,146 Tysv?r 73 71.0 71.0 102.8%
3,103 Moss 275 268.0 268.0 102.6%
5,056 Hitra 40 39.0 39.0 102.6%
3,316 Modum 96 94.0 94.0 102.1%
1,505 Kristiansund 179 176.0 176.0 101.7%
5,058 ?fjord 60 59.0 59.0 101.7%
1,804 Bod? 326 321.0 321.0 101.6%
5,001 Trondheim - Tr?ante 1,513 1,493.0 1,493.0 101.3%
4,005 Notodden 92 91.0 91.0 101.1%
5,028 Melhus 106 105.0 105.0 101.0%
1,106 Haugesund 298 269.0 296.0 100.7%
3,101 Halden 212 211.0 211.0 100.5%
1,833 Rana - Raane 226 225.0 225.0 100.4%
3,232 Nittedal 117 117.0 117.0 100.0%
3,418 ?snes 79 79.0 79.0 100.0%
3,442 ?stre Toten 108 108.0 108.0 100.0%
3,443 Vestre Toten 48 48.0 48.0 100.0%
3,447 S?ndre Land 38 38.0 38.0 100.0%
3,450 Etnedal 23 23.0 23.0 100.0%
3,322 Nesbyen 16 16.0 16.0 100.0%
4,611 Etne 35 35.0 35.0 100.0%
1,525 Stranda 37 37.0 37.0 100.0%
1,528 Sykkylven 38 38.0 38.0 100.0%
5,027 Midtre Gauldal 41 41.0 41.0 100.0%
5,061 Rindal 19 19.0 19.0 100.0%
1,811 Bindal 24 24.0 24.0 100.0%
1,840 Saltdal 41 41.0 41.0 100.0%
1,841 Fauske - Fuosko 85 85.0 85.0 100.0%
5,520 Bardu 37 37.0 37.0 100.0%
5,528 Dyr?y 23 23.0 23.0 100.0%
5,624 Lebesby 17 17.0 17.0 100.0%
4,203 Arendal 304 305.0 305.0 99.7%
1,108 Sandnes 473 475.0 475.0 99.6%
3,407 Gj?vik 143 144.0 144.0 99.3%
3,413 Stange 143 144.0 144.0 99.3%
3,405 Lillehammer 244 246.0 246.0 99.2%
5,054 Indre Fosen 84 85.0 85.0 98.8%
3,436 Nord-Fron 70 71.0 71.0 98.6%
3,238 Nannestad 67 68.0 68.0 98.5%
5,038 Verdal 129 131.0 131.0 98.5%
1,103 Stavanger 1,054 1,027.0 1,071.0 98.4%
3,441 Gausdal 61 62.0 62.0 98.4%
3,427 Tynset 59 60.0 60.0 98.3%
3,118 Indre ?stfold 318 324.0 324.0 98.1%
3,207 Nordre Follo 390 398.0 398.0 98.0%
1,149 Karm?y 293 300.0 300.0 97.7%
3,107 Fredrikstad 585 600.0 600.0 97.5%
3,110 Hvaler 38 39.0 39.0 97.4%
3,330 Hol 38 39.0 39.0 97.4%
3,401 Kongsvinger 173 178.0 178.0 97.2%
4,627 Ask?y 161 166.0 166.0 97.0%
3,220 Enebakk 64 66.0 66.0 97.0%
1,838 Gildesk?l 31 32.0 32.0 96.9%
4,634 Masfjorden 29 30.0 30.0 96.7%
4,601 Bergen 2,451 2,506.0 2,541.0 96.5%
4,649 Stad 80 83.0 83.0 96.4%
1,160 Vindafjord 78 81.0 81.0 96.3%
4,631 Alver 208 216.0 216.0 96.3%
1,508 ?lesund 335 348.0 348.0 96.3%
5,007 Namsos - N?avmesjenjaelmie 135 140.4 140.4 96.2%
1,563 Sunndal 73 76.0 76.0 96.1%
3,424 Rendalen 22 23.0 23.0 95.7%
1,101 Eigersund 66 69.0 69.0 95.7%
4,613 B?mlo 87 91.0 91.0 95.6%
1,127 Randaberg 62 65.0 65.0 95.4%
3,909 Larvik 385 404.0 404.0 95.3%
5,534 Karls?y 40 42.0 42.0 95.2%
1,824 Vefsn 117 123.0 123.0 95.1%
national_proj |>
  filter(year == min(year)) |>
  mutate(population = comma(population)) |>
  kable(caption = paste("Projected elderly population -", min(national_proj$year)),
        col.names = c("Scenario", "Year", "Age band", "Population")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Projected elderly population - 2025
Scenario Year Age band Population
Strong ageing 2025 67-79 667,683
Strong ageing 2025 80-89 225,329
Strong ageing 2025 90+ 45,319
Weak ageing 2025 67-79 666,925
Weak ageing 2025 80-89 224,375
Weak ageing 2025 90+ 44,681

4 Visualisation - national population trend

national_proj |>
  mutate(age_band = factor(age_band, levels = c("90+", "80-89", "67-79"))) |>
  ggplot(aes(x = year, y = population / 1000,
             colour = age_band, linetype = scenario)) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 2) +
  scale_y_continuous(labels = label_comma(suffix = "k")) +
  scale_x_continuous(breaks = seq(2025, 2036, 2)) +
  scale_colour_manual(
    values = c("90+" = "#C00000", "80-89" = "#ED7D31", "67-79" = "#4472C4")
  ) +
  labs(
    title    = "Norwegian elderly population projections 2025-2036",
    subtitle = "Strong vs. weak ageing scenarios by age band",
    x = NULL, y = "Population (thousands)",
    colour = "Age band", linetype = "Scenario"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom", panel.grid.minor = element_blank())


5 Roadmap - what we build next

  1. Use rates (2025 baseline) - NH residents divided by population by age band per municipality. These are the age-specific admission probabilities.

  2. Demand projection - apply use rates to projected populations year by year under both ageing scenarios.

  3. Supply scenarios - flat (no new beds), historical growth (+0.4% per year), and a user-defined expansion scenario.

  4. Crossover detection - first year where projected demand exceeds supply, nationally and per municipality.

  5. Back-calibration - what reduction in use rate keeps demand below the supply ceiling? Expressed as N patients per year that must stay in home care.

  6. Survival model link - plug the back-calibration target into the Markov model to show how much the intervention postpones the crossover year.


6 Session info

sessionInfo()
## R version 4.4.2 (2024-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
## 
## 
## locale:
## [1] LC_COLLATE=German_Germany.utf8  LC_CTYPE=German_Germany.utf8   
## [3] LC_MONETARY=German_Germany.utf8 LC_NUMERIC=C                   
## [5] LC_TIME=German_Germany.utf8    
## 
## time zone: Europe/Berlin
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] jsonlite_2.0.0   servr_0.32       reader_1.1.0     NCmisc_1.3.1    
##  [5] patchwork_1.3.2  scales_1.4.0     kableExtra_1.4.0 knitr_1.51      
##  [9] janitor_2.2.1    lubridate_1.9.4  forcats_1.0.1    stringr_1.6.0   
## [13] dplyr_1.1.4      purrr_1.2.0      readr_2.2.0      tidyr_1.3.2     
## [17] tibble_3.3.0     ggplot2_4.0.1    tidyverse_2.0.0 
## 
## loaded via a namespace (and not attached):
##  [1] sass_0.4.10        generics_0.1.4     xml2_1.5.1         stringi_1.8.7     
##  [5] hms_1.1.4          digest_0.6.37      magrittr_2.0.4     evaluate_1.0.5    
##  [9] grid_4.4.2         timechange_0.3.0   RColorBrewer_1.1-3 fastmap_1.2.0     
## [13] promises_1.5.0     viridisLite_0.4.2  textshaping_1.0.4  jquerylib_0.1.4   
## [17] cli_3.6.5          crayon_1.5.3       rlang_1.1.6        bit64_4.6.0-1     
## [21] withr_3.0.2        cachem_1.1.0       yaml_2.3.12        otel_0.2.0        
## [25] parallel_4.4.2     tools_4.4.2        tzdb_0.5.0         httpuv_1.6.17     
## [29] vctrs_0.6.5        R6_2.6.1           lifecycle_1.0.4    snakecase_0.11.1  
## [33] bit_4.6.0          vroom_1.7.1        pkgconfig_2.0.3    later_1.4.8       
## [37] pillar_1.11.1      bslib_0.9.0        gtable_0.3.6       Rcpp_1.1.1        
## [41] glue_1.8.0         systemfonts_1.3.1  xfun_0.55          tidyselect_1.2.1  
## [45] rstudioapi_0.18.0  farver_2.1.2       htmltools_0.5.9    labeling_0.4.3    
## [49] rmarkdown_2.30     svglite_2.2.2      compiler_4.4.2     S7_0.2.0
  1. Use rates (2025 baseline) - NH residents divided by population by age band per municipality. These are the age-specific admission probabilities.
# ── National 2025 elderly population from projection data ───────────────────
pop_2025 <- projections_banded |>
  filter(year == 2025, scenario == "Strong ageing") |>
  # Both scenarios have identical 2025 values — it's the base year
  group_by(age_band) |>
  summarise(population_2025 = sum(population, na.rm = TRUE), .groups = "drop")

cat("National elderly population 2025:\n")
## National elderly population 2025:
print(pop_2025)
## # A tibble: 3 × 2
##   age_band population_2025
##   <chr>              <dbl>
## 1 67-79             667683
## 2 80-89             225329
## 3 90+                45319
# ── National long-term institutional demand 2025 ─────────────────────────────
national_lt_demand <- users |>
  summarise(
    total_lt_stay    = sum(long_term_stay_in_institutions, na.rm = TRUE),
    total_beds       = sum(institution_all_available_beds, na.rm = TRUE),
    total_dementia   = sum(dementia_beds, na.rm = TRUE)
  )

cat("\nNational long-term institutional demand 2025:\n")
## 
## National long-term institutional demand 2025:
print(national_lt_demand)
## # A tibble: 1 × 3
##   total_lt_stay total_beds total_dementia
##           <dbl>      <dbl>          <dbl>
## 1         30622     41650.          11704
# ── National use rates by age band ───────────────────────────────────────────
# We cannot split long_term_stay by age band from this data directly.
# Instead we use the known national age distribution of NH residents
# from KOSTRA 2025 (from your Residence in institutions sheet in Excel):
#   67-79: ~9,719 residents
#   80-89: ~15,144 residents  
#   90+:   ~10,380 residents
# Adjust these numbers if your Excel data shows different figures.

lt_by_age <- tibble(
  age_band           = c("67-79", "80-89", "90+"),
  lt_residents_2025  = c(9719, 15144, 10380)   # <-- update from your Excel if needed
)

use_rates <- lt_by_age |>
  left_join(pop_2025, by = "age_band") |>
  mutate(
    use_rate = lt_residents_2025 / population_2025
  )

cat("\nAge-specific use rates (probability of being in LT institutional care):\n")
## 
## Age-specific use rates (probability of being in LT institutional care):
print(use_rates)
## # A tibble: 3 × 4
##   age_band lt_residents_2025 population_2025 use_rate
##   <chr>                <dbl>           <dbl>    <dbl>
## 1 67-79                 9719          667683   0.0146
## 2 80-89                15144          225329   0.0672
## 3 90+                  10380           45319   0.229
# ── Corrected LT institutional use rates ─────────────────────────────────────
# Scale the age-band breakdown to match the actual total_lt_stay from the data

actual_total_lt <- national_lt_demand$total_lt_stay   # 30,622
assumed_total   <- 9719 + 15144 + 10380               # 35,243
scale_factor    <- actual_total_lt / assumed_total

lt_by_age <- tibble(
  age_band          = c("67-79", "80-89", "90+"),
  lt_residents_2025 = round(c(9719, 15144, 10380) * scale_factor)
)

use_rates_institutional <- lt_by_age |>
  left_join(pop_2025, by = "age_band") |>
  mutate(
    use_rate_institutional = lt_residents_2025 / population_2025
  )

cat("Corrected institutional use rates:\n")
## Corrected institutional use rates:
print(use_rates_institutional)
## # A tibble: 3 × 4
##   age_band lt_residents_2025 population_2025 use_rate_institutional
##   <chr>                <dbl>           <dbl>                  <dbl>
## 1 67-79                 8445          667683                 0.0126
## 2 80-89                13158          225329                 0.0584
## 3 90+                   9019           45319                 0.199
# ── Home care use rates ───────────────────────────────────────────────────────
# We need elderly home care users by age band.
# From users dataset: home_care_elderly = total - under 67
# But we need the 67-79 / 80-89 / 90+ split.
# We can get this from the projection files indirectly via the services data,
# but we do not have it split by age in services_raw.
# Best available proxy: use the age distribution from SSB table 11645
# which shows home nursing users by age (from your Users sheet in the Excel).
# Known approximate national split for home-based services elderly users:
#   67-79: ~36% of elderly home care users
#   80-89: ~44% of elderly home care users
#   90+:   ~20% of elderly home care users
# These come from your DifferentDatas sheet analysis earlier in this project.

total_home_care_elderly <- sum(users$home_care_elderly, na.rm = TRUE)
cat("\nTotal elderly home care users (national):", comma(total_home_care_elderly), "\n")
## 
## Total elderly home care users (national): 113,343
# Also compute total home nursing and practical assistance from services data
total_home_nursing <- sum(users$home_nursing, na.rm = TRUE)
total_practical    <- sum(users$practical_assistance_daily_activities, na.rm = TRUE)

cat("Total home nursing users:                ", comma(total_home_nursing), "\n")
## Total home nursing users:                 168,586
cat("Total practical assistance users:        ", comma(total_practical), "\n")
## Total practical assistance users:         75,068
# Age-band split for home care
hc_by_age <- tibble(
  age_band        = c("67-79", "80-89", "90+"),
  hc_share        = c(0.367,    0.298,   0.247),   # from your DifferentDatas analysis
  hc_users_2025   = round(total_home_care_elderly * hc_share / sum(c(0.367, 0.298, 0.247)))
)

use_rates_homecare <- hc_by_age |>
  left_join(pop_2025, by = "age_band") |>
  mutate(
    use_rate_homecare = hc_users_2025 / population_2025
  )

cat("\nHome care use rates by age band:\n")
## 
## Home care use rates by age band:
print(use_rates_homecare)
## # A tibble: 3 × 5
##   age_band hc_share hc_users_2025 population_2025 use_rate_homecare
##   <chr>       <dbl>         <dbl>           <dbl>             <dbl>
## 1 67-79       0.367         45611          667683            0.0683
## 2 80-89       0.298         37035          225329            0.164 
## 3 90+         0.247         30697           45319            0.677
# ── Combined use rate summary ─────────────────────────────────────────────────
use_rates_combined <- use_rates_institutional |>
  select(age_band, lt_residents_2025, use_rate_institutional) |>
  left_join(
    use_rates_homecare |> select(age_band, hc_users_2025, use_rate_homecare),
    by = "age_band"
  ) |>
  left_join(pop_2025, by = "age_band") |>
  mutate(
    total_care_users  = lt_residents_2025 + hc_users_2025,
    use_rate_any_care = total_care_users / population_2025,
    pct_institutional = lt_residents_2025 / total_care_users
  )

cat("\nCombined use rate summary:\n")
## 
## Combined use rate summary:
use_rates_combined |>
  mutate(across(starts_with("use_rate"), ~ percent(.x, accuracy = 0.1)),
         across(where(is.numeric), ~ comma(.x))) |>
  kable(caption = "2025 care service use rates by elderly age band",
        col.names = c("Age band", "LT institutional users", "Use rate (institutional)",
                      "Home care users", "Use rate (home care)",
                      "Population 2025", "Total care users",
                      "Use rate (any care)", "Share in institutions")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
2025 care service use rates by elderly age band
Age band LT institutional users Use rate (institutional) Home care users Use rate (home care) Population 2025 Total care users Use rate (any care) Share in institutions
67-79 8,445 1.3% 45,611 6.8% 667,683 54,056 8.1% 0.156
80-89 13,158 5.8% 37,035 16.4% 225,329 50,193 22.3% 0.262
90+ 9,019 19.9% 30,697 67.7% 45,319 39,716 87.6% 0.227
  1. Demand projection - apply use rates to projected populations year by year under both ageing scenarios.
# ════════════════════════════════════════════════════════════════════════════
# DEMAND PROJECTION
# ════════════════════════════════════════════════════════════════════════════

# ── Parameters (adjust these to test different scenarios) ───────────────────
SUPPLY_GROWTH_RATE  <- 0.004   # historical +0.4% per year
EXPANSION_ANNUAL    <- 500     # extra beds added per year in expansion scenario
MIN_POP_THRESHOLD   <- 50      # minimum elderly pop per band for stable use rate
# ── Step 1: National use rates (already computed above) ─────────────────────
# use_rates_institutional has: age_band, use_rate_institutional
national_use_rates <- use_rates_institutional |>
  select(age_band, use_rate = use_rate_institutional)
# ── Step 2: National demand projection ──────────────────────────────────────
national_demand <- national_proj |>
  left_join(national_use_rates, by = "age_band") |>
  mutate(projected_lt_users = population * use_rate) |>
  group_by(scenario, year) |>
  summarise(
    projected_demand = sum(projected_lt_users, na.rm = TRUE),
    .groups = "drop"
  )

# National supply scenarios
national_supply_base <- sum(users$institution_all_available_beds, na.rm = TRUE)

national_supply <- tibble(year = 2025:2036) |>
  mutate(
    flat             = national_supply_base,
    historical       = national_supply_base * (1 + SUPPLY_GROWTH_RATE)^(year - 2025),
    expansion        = national_supply_base + EXPANSION_ANNUAL * (year - 2025)
  ) |>
  pivot_longer(-year, names_to = "supply_scenario", values_to = "supply")
# ── Combine demand and supply ────────────────────────────────────────────────
national_combined <- national_demand |>
  left_join(national_supply, by = "year",
            relationship= "many-to-many") |>
  mutate(
    gap           = projected_demand - supply,
    over_capacity = gap > 0
  )

cat("National supply baseline (2025):", comma(national_supply_base), "beds\n")
## National supply baseline (2025): 41,650 beds
cat("National LT demand (2025):      ",
    comma(round(
      national_combined |>
        filter(year == 2025,
               scenario == "Strong ageing",
               supply_scenario == "flat") |>
        pull(projected_demand)
    )), "\n")
## National LT demand (2025):       30,622
# Clean join
national_combined <- national_demand |>
  left_join(national_supply, by = "year",
            relationship = "many-to-many") |>
  mutate(
    gap           = projected_demand - supply,
    over_capacity = gap > 0
  )

cat("National supply baseline (2025):", comma(national_supply_base), "beds\n")
## National supply baseline (2025): 41,650 beds
cat("National LT demand (2025):      ",
    comma(round(filter(national_combined, year == 2025,
                       scenario == "Strong ageing",
                       supply_scenario == "flat")$projected_demand)), "\n")
## National LT demand (2025):       30,622
# ── Step 3: National crossover year ─────────────────────────────────────────
national_crossover <- national_combined |>
  filter(over_capacity) |>
  group_by(scenario, supply_scenario) |>
  summarise(crossover_year = min(year), .groups = "drop")

cat("\nNational crossover years (demand exceeds supply):\n")
## 
## National crossover years (demand exceeds supply):
national_crossover |>
  pivot_wider(names_from = supply_scenario,
              values_from = crossover_year,
              values_fill = NA) |>
  kable(caption = "First year projected LT demand exceeds supply — national") |>
  kable_styling(bootstrap_options = c("striped", "hover"))
First year projected LT demand exceeds supply — national
scenario flat historical
Strong ageing 2034 2034
# ── Step 4: Municipality use rates ──────────────────────────────────────────
# For each municipality: use rate = lt_residents_2025 / population_2025
# We do not have lt_residents split by age band per municipality,
# so we apply the NATIONAL age-band use rates scaled by the municipality's
# overall lt intensity (their total LT stay / national average LT stay per capita)

national_lt_per_capita <- sum(users$long_term_stay_in_institutions, na.rm = TRUE) /
                          sum(pop_2025$population_2025)

muni_use_rates <- users |>
  select(muni_code, muni_name, long_term_stay_in_institutions,
         institution_all_available_beds) |>
  mutate(
    # Municipality-specific scaling factor relative to national average
    lt_per_capita  = long_term_stay_in_institutions /
                     sum(long_term_stay_in_institutions, na.rm = TRUE) *
                     sum(pop_2025$population_2025),
    muni_scale     = (long_term_stay_in_institutions /
                      replace_na(long_term_stay_in_institutions, 0)) /
                     (national_lt_per_capita * 1),
  ) |>
  # Simpler and more robust: just compute LT users per elderly resident
  # using the municipality's own total elderly population from projections
  select(muni_code, muni_name, long_term_stay_in_institutions,
         supply_2025 = institution_all_available_beds)
# Get 2025 elderly population per municipality from projections
muni_pop_2025 <- projections_banded |>
  filter(year == 2025, scenario == "Strong ageing") |>
  group_by(muni_code, age_band) |>
  summarise(population = sum(population, na.rm = TRUE), .groups = "drop")

muni_pop_2025_total <- muni_pop_2025 |>
  group_by(muni_code) |>
  summarise(elderly_pop_2025 = sum(population), .groups = "drop")
# Join and compute municipality use rate (aggregate, not age-banded)
muni_use_rates <- muni_use_rates |>
  left_join(muni_pop_2025_total, by = "muni_code") |>
  mutate(
    use_rate_muni = long_term_stay_in_institutions / elderly_pop_2025,
    # Flag municipalities with too few elderly for stable rate
    stable        = elderly_pop_2025 >= MIN_POP_THRESHOLD
  ) |>
  filter(!is.na(use_rate_muni))

cat("\nMunicipality use rates — summary:\n")
## 
## Municipality use rates — summary:
summary(muni_use_rates$use_rate_muni)
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
## 0.005995 0.027099 0.032022 0.033312 0.038778 0.085938
# ── Step 5: Municipality demand projection ───────────────────────────────────
# For each municipality: apply their own use rate to projected elderly pop
# For small municipalities (stable == FALSE): use national age-band rates instead

muni_proj_pop <- projections_banded |>
  group_by(muni_code, scenario, year) |>
  summarise(elderly_pop = sum(population, na.rm = TRUE), .groups = "drop")

muni_demand <- muni_proj_pop |>
  left_join(muni_use_rates |> select(muni_code, use_rate_muni,
                                      supply_2025, stable, muni_name),
            by = "muni_code") |>
  # Fall back to national aggregate use rate for unstable municipalities
  mutate(
    use_rate_applied = if_else(
      stable,
      use_rate_muni,
      sum(users$long_term_stay_in_institutions, na.rm = TRUE) /
        sum(muni_pop_2025_total$elderly_pop_2025, na.rm = TRUE)
    ),
    projected_demand = elderly_pop * use_rate_applied
  )
# ── Step 6: Municipality supply scenarios ────────────────────────────────────
muni_supply <- muni_demand |>
  distinct(muni_code, supply_2025) |>
  crossing(year = 2025:2036) |>
  mutate(
    flat       = supply_2025,
    historical = supply_2025 * (1 + SUPPLY_GROWTH_RATE)^(year - 2025),
    expansion  = supply_2025 + (EXPANSION_ANNUAL *
                   (supply_2025 / national_supply_base)) * (year - 2025)
  ) |>
  pivot_longer(c(flat, historical, expansion),
               names_to = "supply_scenario", values_to = "supply")

muni_combined <- muni_demand |>
  left_join(muni_supply, by = c("muni_code", "year")) |>
  mutate(
    gap           = projected_demand - supply,
    over_capacity = gap > 0
  )
# ── Step 7: Municipality crossover years ─────────────────────────────────────
muni_crossover <- muni_combined |>
  filter(over_capacity) |>
  group_by(muni_code, muni_name, scenario, supply_scenario) |>
  summarise(crossover_year = min(year), .groups = "drop")
# Summary table: how many municipalities cross over by year
cat("\nMunicipalities crossing over — national summary (flat supply, strong ageing):\n")
## 
## Municipalities crossing over — national summary (flat supply, strong ageing):
muni_crossover |>
  filter(scenario == "Strong ageing", supply_scenario == "flat") |>
  count(crossover_year) |>
  arrange(crossover_year) |>
  mutate(cumulative = cumsum(n)) |>
  kable(caption = "Number of municipalities where demand exceeds supply (flat supply, strong ageing)",
        col.names = c("Year", "New crossovers", "Cumulative")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Number of municipalities where demand exceeds supply (flat supply, strong ageing)
Year New crossovers Cumulative
2025 1 1
2026 3 4
2027 1 5
2028 1 6
2029 2 8
2030 4 12
2031 7 19
2032 4 23
2033 9 32
2034 8 40
# Top 20 most urgent municipalities
cat("\nMost urgent municipalities (earliest crossover, flat supply, strong ageing):\n")
## 
## Most urgent municipalities (earliest crossover, flat supply, strong ageing):
muni_crossover |>
  filter(scenario == "Strong ageing", supply_scenario == "flat") |>
  arrange(crossover_year) |>
  head(20) |>
  left_join(users |> select(muni_code, institution_all_available_beds,
                             long_term_stay_in_institutions),
            by = "muni_code") |>
  kable(caption = "20 most urgent municipalities — earliest demand-supply crossover",
        format.args = list(big.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
20 most urgent municipalities — earliest demand-supply crossover
muni_code muni_name scenario supply_scenario crossover_year institution_all_available_beds long_term_stay_in_institutions
5,536 Lyngen - Ivgu - Yyke? Strong ageing flat 2,025 24.0 29
1,145 Bokn Strong ageing flat 2,026 9.0 9
1,840 Saltdal Strong ageing flat 2,026 41.0 41
5,620 Nordkapp Strong ageing flat 2,026 28.0 28
5,622 Porsanger - Pors?ngu - Porsanki Strong ageing flat 2,027 36.0 34
5,610 K?r?sjohka - Karasjok Strong ageing flat 2,028 16.0 15
301 Oslo - Oslove Strong ageing flat 2,029 4,121.1 3,759
1,828 Nesna Strong ageing flat 2,029 15.0 14
1,532 Giske Strong ageing flat 2,030 24.0 21
3,222 L?renskog Strong ageing flat 2,030 240.0 206
3,336 Rollag Strong ageing flat 2,030 16.0 15
5,544 Nordreisa - R?isa - Raisi Strong ageing flat 2,030 52.0 48
1,160 Vindafjord Strong ageing flat 2,031 81.0 70
1,838 Gildesk?l Strong ageing flat 2,031 32.0 29
3,112 R?de Strong ageing flat 2,031 51.0 46
3,238 Nannestad Strong ageing flat 2,031 68.0 56
4,614 Stord Strong ageing flat 2,031 109.0 95
5,029 Skaun Strong ageing flat 2,031 50.0 42
5,534 Karls?y Strong ageing flat 2,031 42.0 38
3,220 Enebakk Strong ageing flat 2,032 71.0 57
  1. Back-calibration - what reduction in use rate keeps demand below the supply ceiling? Expressed as N patients per year that must stay in home care.
# ════════════════════════════════════════════════════════════════════════════
# BACK-CALIBRATION
# How many institutional admissions need to be prevented each year?
# ════════════════════════════════════════════════════════════════════════════

# ── Step 1: National back-calibration ───────────────────────────────────────

# ── Step 1: Total projected elderly population by scenario and year ──────────
national_elderly_proj <- national_proj |>
  group_by(scenario, year) |>
  summarise(total_elderly = sum(population, na.rm = TRUE), .groups = "drop")
# ── Step 2: National back-calibration table ──────────────────────────────────
national_backcalib <- national_combined |>
  filter(supply_scenario == "flat") |>
  left_join(national_elderly_proj, by = c("scenario", "year")) |>
  mutate(
    # Use rate that would keep demand exactly at supply
    required_use_rate     = supply / total_elderly,

    # Current use rate (what we actually observe)
    current_use_rate      = projected_demand / total_elderly,

    # How many admissions need to be prevented (positive = action needed)
    admissions_to_prevent = projected_demand - supply,

    # What percentage reduction in admissions does that represent
    pct_reduction_needed  = admissions_to_prevent / projected_demand * 100,

    # Use rate reduction needed (percentage points)
    use_rate_reduction_pp = (current_use_rate - required_use_rate) * 100
  )
# Print national summary
cat("National back-calibration (flat supply, both scenarios):\n")
## National back-calibration (flat supply, both scenarios):
national_backcalib |>
  filter(year >= 2028) |>
  select(scenario, year, projected_demand, supply,
         admissions_to_prevent, pct_reduction_needed) |>
  mutate(
    projected_demand      = comma(round(projected_demand)),
    supply                = comma(round(supply)),
    admissions_to_prevent = comma(round(admissions_to_prevent)),
    pct_reduction_needed  = round(pct_reduction_needed, 1)
  ) |>
  kable(
    caption   = "National back-calibration — admissions to prevent per year (flat supply)",
    col.names = c("Scenario", "Year", "Projected demand",
                  "Supply", "Admissions to prevent", "% reduction needed")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover")) |>
  row_spec(which(as.numeric(
    str_extract(national_backcalib |>
                  filter(year >= 2028) |>
                  pull(admissions_to_prevent), "-?\\d+"
    )) > 0), background = "#FCE4D6")
National back-calibration — admissions to prevent per year (flat supply)
Scenario Year Projected demand Supply Admissions to prevent % reduction needed
Strong ageing 2028 34,446 41,650 -7,205 -20.9
Strong ageing 2029 35,861 41,650 -5,790 -16.1
Strong ageing 2030 37,267 41,650 -4,384 -11.8
Strong ageing 2031 38,704 41,650 -2,946 -7.6
Strong ageing 2032 40,015 41,650 -1,636 -4.1
Strong ageing 2033 41,636 41,650 -14 0.0
Strong ageing 2034 43,405 41,650 1,755 4.0
Weak ageing 2028 33,292 41,650 -8,358 -25.1
Weak ageing 2029 34,298 41,650 -7,352 -21.4
Weak ageing 2030 35,280 41,650 -6,370 -18.1
Weak ageing 2031 36,242 41,650 -5,409 -14.9
Weak ageing 2032 37,050 41,650 -4,600 -12.4
Weak ageing 2033 38,106 41,650 -3,544 -9.3
Weak ageing 2034 39,236 41,650 -2,415 -6.2
# ── Step 3: Municipality back-calibration ────────────────────────────────────
muni_elderly_proj <- projections_banded |>
  group_by(muni_code, scenario, year) |>
  summarise(total_elderly = sum(population, na.rm = TRUE), .groups = "drop")

muni_backcalib <- muni_combined |>
  filter(supply_scenario == "flat") |>
  left_join(
    muni_elderly_proj,
    by = c("muni_code", "scenario", "year")
  ) |>
  left_join(muni_lookup, by = "muni_code") |>
  # Clean up duplicate columns from the joins
  rename(scenario  = scenario,
         muni_name = muni_name.y) |>
  select(-any_of(c("muni_name.x", "scenario.x", "scenario.y",
                    "muni_name", "supply_2025.x", "supply_2025.y"))) |>
  mutate(
    required_use_rate     = supply / total_elderly,
    current_use_rate      = projected_demand / total_elderly,
    admissions_to_prevent = projected_demand - supply,
    pct_reduction_needed  = admissions_to_prevent / projected_demand * 100,
    use_rate_reduction_pp = (current_use_rate - required_use_rate) * 100
  )
muni_backcalib <- muni_combined |>
  filter(supply_scenario == "flat") |>
  left_join(
    muni_elderly_proj,
    by = c("muni_code", "scenario", "year")
  ) |>
  select(-any_of(c("muni_name", "muni_name.x", "muni_name.y",
                    "supply_2025.x", "supply_2025.y",
                    "scenario.x", "scenario.y"))) |>
  mutate(
    required_use_rate     = supply / total_elderly,
    current_use_rate      = projected_demand / total_elderly,
    admissions_to_prevent = projected_demand - supply,
    pct_reduction_needed  = admissions_to_prevent / projected_demand * 100,
    use_rate_reduction_pp = (current_use_rate - required_use_rate) * 100
  ) |>
  left_join(muni_lookup, by = "muni_code")   # add name at the very end
# ── Step 4: Municipality summary table ───────────────────────────────────────
# Show each municipality's situation in their crossover year
# (the first year action is needed)

muni_action_needed <- muni_backcalib |>
  filter(admissions_to_prevent > 0, scenario == "Strong ageing") |>
  group_by(muni_code, muni_name) |>
  slice_min(year, n = 1) |>          # first year action is needed
  ungroup() |>
  arrange(year, desc(admissions_to_prevent)) |>
  select(muni_code, muni_name, year,
         total_elderly, projected_demand, supply,
         admissions_to_prevent, pct_reduction_needed,
         use_rate_reduction_pp)

cat("\nMunicipalities requiring action — first year and scale of intervention:\n")
## 
## Municipalities requiring action — first year and scale of intervention:
muni_action_needed |>
  mutate(
    total_elderly         = comma(round(total_elderly)),
    projected_demand      = comma(round(projected_demand)),
    supply                = comma(round(supply)),
    admissions_to_prevent = comma(round(admissions_to_prevent)),
    pct_reduction_needed  = paste0(round(pct_reduction_needed, 1), "%"),
    use_rate_reduction_pp = paste0(round(use_rate_reduction_pp, 3), " pp")
  ) |>
  select(-muni_code) |>
  kable(
    caption   = "Municipality back-calibration — action required (strong ageing, flat supply)",
    col.names = c("Municipality", "Action needed from", "Elderly population",
                  "Projected demand", "Supply", "Admissions to prevent",
                  "% reduction needed", "Use rate reduction (pp)")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = TRUE, font_size = 11)
Municipality back-calibration — action required (strong ageing, flat supply)
Municipality Action needed from Elderly population Projected demand Supply Admissions to prevent % reduction needed Use rate reduction (pp)
Lyngen - Ivgu - Yyke? 2025 748 29 24 5 17.2% 0.668 pp
Bokn 2026 201 10 9 1 6% 0.284 pp
Saltdal 2026 1,133 42 41 1 1.3% 0.049 pp
Nordkapp 2026 619 28 28 0 1.5% 0.067 pp
Porsanger - Pors?ngu - Porsanki 2027 911 36 36 0 0.2% 0.006 pp
K?r?sjohka - Karasjok 2028 563 17 16 1 3.9% 0.117 pp
Oslo - Oslove 2029 95,450 4,217 4,121 95 2.3% 0.1 pp
Nesna 2029 406 15 15 0 2.6% 0.099 pp
L?renskog 2030 8,071 244 240 4 1.7% 0.052 pp
Giske 2030 1,625 24 24 0 0.8% 0.012 pp
Rollag 2030 408 16 16 0 0.9% 0.036 pp
Nordreisa - R?isa - Raisi 2030 1,092 52 52 0 0.1% 0.005 pp
Nannestad 2031 2,476 69 68 1 1.1% 0.03 pp
Stord 2031 3,716 110 109 1 0.7% 0.02 pp
R?de 2031 1,759 52 51 1 1.4% 0.04 pp
Vindafjord 2031 1,855 81 81 0 0.4% 0.019 pp
Karls?y 2031 679 42 42 0 0.4% 0.024 pp
Gildesk?l 2031 591 32 32 0 0.3% 0.016 pp
Skaun 2031 1,490 50 50 0 0.1% 0.004 pp
Nittedal 2032 4,420 125 124 2 1.3% 0.037 pp
Enebakk 2032 2,135 72 71 1 1.9% 0.063 pp
B?tsfjord 2032 428 17 16 1 3.6% 0.14 pp
Bardu 2032 832 37 37 0 0% 0 pp
Lillestr?m 2033 16,892 490 473 17 3.4% 0.099 pp
Stavanger 2033 26,841 1,136 1,119 17 1.5% 0.062 pp
Sandnes 2033 14,180 523 518 5 0.9% 0.034 pp
Nes 2033 5,067 129 125 4 2.8% 0.071 pp
Trondheim - Tr?ante 2033 37,366 1,545 1,542 3 0.2% 0.009 pp
R?lingen 2033 3,340 89 86 3 3.1% 0.081 pp
Sola 2033 4,904 188 187 1 0.4% 0.014 pp
Gausdal 2033 1,599 63 63 0 0.8% 0.031 pp
Hol 2033 1,199 39 39 0 0.5% 0.018 pp
Kristiansand 2034 22,932 735 726 9 1.2% 0.04 pp
Bergen 2034 54,563 2,615 2,609 6 0.2% 0.011 pp
Troms? 2034 14,272 478 474 4 0.8% 0.027 pp
Vennesla 2034 3,043 101 100 2 1.6% 0.052 pp
Halden 2034 7,807 219 218 1 0.5% 0.013 pp
Alstahaug 2034 1,839 57 56 1 1.5% 0.048 pp
Sunndal 2034 1,851 78 78 0 0.2% 0.008 pp
Lur?y 2034 502 13 13 0 0.1% 0.002 pp
# ── Step 4b: Municipality trajectory — 5 years beyond crossover ──────────────

# Get each municipality's crossover year
muni_crossover_year <- muni_backcalib |>
  filter(admissions_to_prevent > 0, scenario == "Strong ageing") |>
  group_by(muni_code) |>
  summarise(crossover_year = min(year), .groups = "drop")

# Filter backcalib to crossover year through min(crossover + 5, 2036)
muni_trajectory <- muni_backcalib |>
  filter(scenario == "Strong ageing") |>
  left_join(muni_crossover_year, by = "muni_code") |>
  filter(
    !is.na(crossover_year),                        # only municipalities that cross over
    year >= crossover_year,                        # from crossover year onwards
    year <= pmin(crossover_year + 5, 2036)         # up to 5 years after or 2036
  ) |>
  arrange(crossover_year, muni_code, year) |>
  select(muni_code, muni_name, crossover_year, year,
         total_elderly, projected_demand, supply,
         admissions_to_prevent, pct_reduction_needed,
         use_rate_reduction_pp)

cat("\nMunicipality trajectories from crossover year (strong ageing, flat supply):\n")
## 
## Municipality trajectories from crossover year (strong ageing, flat supply):
muni_trajectory |>
  mutate(
    total_elderly         = comma(round(total_elderly)),
    projected_demand      = comma(round(projected_demand)),
    supply                = comma(round(supply)),
    admissions_to_prevent = comma(round(admissions_to_prevent)),
    pct_reduction_needed  = paste0(round(pct_reduction_needed, 1), "%"),
    use_rate_reduction_pp = paste0(round(use_rate_reduction_pp, 3), " pp")
  ) |>
  select(-muni_code, -crossover_year) |>
  kable(
    caption   = "Municipality trajectories from crossover year + 5 years (strong ageing, flat supply)",
    col.names = c("Municipality", "Year", "Elderly population",
                  "Projected demand", "Supply", "Admissions to prevent",
                  "% reduction needed", "Use rate reduction (pp)")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = TRUE, font_size = 11) |>
  collapse_rows(columns = 1, valign = "top")
Municipality trajectories from crossover year + 5 years (strong ageing, flat supply)
Municipality Year Elderly population Projected demand Supply Admissions to prevent % reduction needed Use rate reduction (pp)
Lyngen - Ivgu - Yyke? 2025 748 29 24 5 17.2% 0.668 pp
2026 753 29 24 5 17.8% 0.69 pp
2027 768 30 24 6 19.4% 0.752 pp
2028 777 30 24 6 20.3% 0.788 pp
2029 784 30 24 6 21% 0.816 pp
2030 786 30 24 6 21.2% 0.824 pp
Bokn 2026 201 10 9 1 6% 0.284 pp
2027 203 10 9 1 6.9% 0.328 pp
2028 208 10 9 1 9.1% 0.435 pp
2029 210 10 9 1 10% 0.476 pp
2030 209 10 9 1 9.6% 0.456 pp
2031 211 10 9 1 10.4% 0.497 pp
Saltdal 2026 1,133 42 41 1 1.3% 0.049 pp
2027 1,139 42 41 1 1.8% 0.068 pp
2028 1,148 42 41 1 2.6% 0.096 pp
2029 1,170 43 41 2 4.4% 0.163 pp
2030 1,205 44 41 3 7.2% 0.265 pp
2031 1,216 45 41 4 8.1% 0.296 pp
Nordkapp 2026 619 28 28 0 1.5% 0.067 pp
2027 638 29 28 1 4.4% 0.201 pp
2028 657 30 28 2 7.2% 0.328 pp
2029 671 31 28 3 9.1% 0.417 pp
2030 682 31 28 3 10.6% 0.485 pp
2031 699 32 28 4 12.7% 0.584 pp
Porsanger - Pors?ngu - Porsanki 2027 911 36 36 0 0.2% 0.006 pp
2028 926 37 36 1 1.8% 0.07 pp
2029 945 37 36 1 3.8% 0.149 pp
2030 965 38 36 2 5.7% 0.228 pp
2031 980 39 36 3 7.2% 0.285 pp
2032 1,002 40 36 4 9.2% 0.365 pp
K?r?sjohka - Karasjok 2028 563 17 16 1 3.9% 0.117 pp
2029 578 17 16 1 6.4% 0.19 pp
2030 589 17 16 1 8.2% 0.242 pp
2031 612 18 16 2 11.6% 0.344 pp
2032 626 19 16 3 13.6% 0.403 pp
2033 642 19 16 3 15.8% 0.466 pp
Oslo - Oslove 2029 95,450 4,217 4,121 95 2.3% 0.1 pp
2030 98,133 4,335 4,121 214 4.9% 0.218 pp
2031 100,949 4,459 4,121 338 7.6% 0.335 pp
2032 103,916 4,591 4,121 469 10.2% 0.452 pp
2033 106,889 4,722 4,121 601 12.7% 0.562 pp
2034 109,868 4,853 4,121 732 15.1% 0.667 pp
Nesna 2029 406 15 15 0 2.6% 0.099 pp
2030 415 16 15 1 4.7% 0.18 pp
2031 424 16 15 1 6.8% 0.256 pp
2032 434 16 15 1 8.9% 0.338 pp
2033 444 17 15 2 11% 0.416 pp
2034 452 17 15 2 12.5% 0.475 pp
Giske 2030 1,625 24 24 0 0.8% 0.012 pp
2031 1,661 25 24 1 3% 0.044 pp
2032 1,699 25 24 1 5.2% 0.077 pp
2033 1,729 26 24 2 6.8% 0.101 pp
2034 1,760 26 24 2 8.4% 0.126 pp
L?renskog 2030 8,071 244 240 4 1.7% 0.052 pp
2031 8,350 253 240 13 5% 0.152 pp
2032 8,670 262 240 22 8.5% 0.258 pp
2033 8,964 271 240 31 11.5% 0.348 pp
2034 9,269 280 240 40 14.4% 0.437 pp
Rollag 2030 408 16 16 0 0.9% 0.036 pp
2031 415 16 16 0 2.6% 0.102 pp
2032 419 17 16 1 3.5% 0.139 pp
2033 429 17 16 1 5.8% 0.228 pp
2034 429 17 16 1 5.8% 0.228 pp
Nordreisa - R?isa - Raisi 2030 1,092 52 52 0 0.1% 0.005 pp
2031 1,097 52 52 0 0.6% 0.026 pp
2032 1,130 54 52 2 3.5% 0.165 pp
2033 1,148 55 52 3 5% 0.237 pp
2034 1,161 55 52 3 6% 0.288 pp
Vindafjord 2031 1,855 81 81 0 0.4% 0.019 pp
2032 1,904 84 81 3 3% 0.132 pp
2033 1,955 86 81 5 5.5% 0.243 pp
2034 1,975 87 81 6 6.5% 0.285 pp
Gildesk?l 2031 591 32 32 0 0.3% 0.016 pp
2032 600 33 32 1 1.8% 0.097 pp
2033 605 33 32 1 2.6% 0.141 pp
2034 615 33 32 1 4.2% 0.227 pp
R?de 2031 1,759 52 51 1 1.4% 0.04 pp
2032 1,793 53 51 2 3.2% 0.095 pp
2033 1,836 54 51 3 5.5% 0.162 pp
2034 1,878 55 51 4 7.6% 0.224 pp
Nannestad 2031 2,476 69 68 1 1.1% 0.03 pp
2032 2,596 72 68 4 5.7% 0.157 pp
2033 2,699 75 68 7 9.3% 0.257 pp
2034 2,820 78 68 10 13.1% 0.365 pp
Stord 2031 3,716 110 109 1 0.7% 0.02 pp
2032 3,810 113 109 4 3.1% 0.092 pp
2033 3,914 116 109 7 5.7% 0.168 pp
2034 4,044 119 109 10 8.7% 0.258 pp
Skaun 2031 1,490 50 50 0 0.1% 0.004 pp
2032 1,542 52 50 2 3.5% 0.117 pp
2033 1,599 54 50 4 6.9% 0.233 pp
2034 1,657 56 50 6 10.2% 0.342 pp
Karls?y 2031 679 42 42 0 0.4% 0.024 pp
2032 697 43 42 1 3% 0.183 pp
2033 708 44 42 2 4.5% 0.277 pp
2034 715 44 42 2 5.4% 0.335 pp
Enebakk 2032 2,135 72 71 1 1.9% 0.063 pp
2033 2,207 75 71 4 5.1% 0.172 pp
2034 2,276 77 71 6 7.9% 0.269 pp
Nittedal 2032 4,420 125 124 2 1.3% 0.037 pp
2033 4,572 130 124 6 4.6% 0.13 pp
2034 4,757 135 124 11 8.3% 0.235 pp
Bardu 2032 832 37 37 0 0% 0 pp
2033 849 38 37 1 2% 0.089 pp
2034 856 38 37 1 2.8% 0.125 pp
B?tsfjord 2032 428 17 16 1 3.6% 0.14 pp
2033 429 17 16 1 3.8% 0.149 pp
2034 439 17 16 1 6% 0.233 pp
Stavanger 2033 26,841 1,136 1,119 17 1.5% 0.062 pp
2034 27,585 1,167 1,119 48 4.1% 0.175 pp
Sandnes 2033 14,180 523 518 5 0.9% 0.034 pp
2034 14,620 539 518 21 3.9% 0.144 pp
Sola 2033 4,904 188 187 1 0.4% 0.014 pp
2034 5,062 194 187 7 3.5% 0.133 pp
Lillestr?m 2033 16,892 490 473 17 3.4% 0.099 pp
2034 17,540 509 473 36 7% 0.202 pp
R?lingen 2033 3,340 89 86 3 3.1% 0.081 pp
2034 3,456 92 86 6 6.3% 0.168 pp
Nes 2033 5,067 129 125 4 2.8% 0.071 pp
2034 5,244 133 125 8 6.1% 0.154 pp
Hol 2033 1,199 39 39 0 0.5% 0.018 pp
2034 1,221 40 39 1 2.3% 0.076 pp
Gausdal 2033 1,599 63 63 0 0.8% 0.031 pp
2034 1,633 65 63 2 2.8% 0.113 pp
Trondheim - Tr?ante 2033 37,366 1,545 1,542 3 0.2% 0.009 pp
2034 38,378 1,587 1,542 45 2.8% 0.118 pp
Sunndal 2034 1,851 78 78 0 0.2% 0.008 pp
Alstahaug 2034 1,839 57 56 1 1.5% 0.048 pp
Lur?y 2034 502 13 13 0 0.1% 0.002 pp
Halden 2034 7,807 219 218 1 0.5% 0.013 pp
Kristiansand 2034 22,932 735 726 9 1.2% 0.04 pp
Vennesla 2034 3,043 101 100 2 1.6% 0.052 pp
Bergen 2034 54,563 2,615 2,609 6 0.2% 0.011 pp
Troms? 2034 14,272 478 474 4 0.8% 0.027 pp
# ── Step 5: Trajectory plot for selected municipalities ──────────────────────
# Show demand vs supply over time for the 6 most urgent municipalities

top6_munis <- muni_crossover |>
  filter(scenario == "Strong ageing", supply_scenario == "flat") |>
  arrange(crossover_year) |>
  slice_head(n = 10) |>
  pull(muni_code)

muni_backcalib |>
  filter(muni_code %in% top6_munis, scenario == "Strong ageing") |>
  left_join(muni_lookup, by = "muni_code") |>
  ggplot(aes(x = year)) +
  geom_line(aes(y = projected_demand, colour = "Projected demand"),
            linewidth = 1.1) +
  geom_line(aes(y = supply, colour = "Supply (flat)"),
            linewidth = 1.1, linetype = "dashed") +
  geom_ribbon(aes(ymin = supply, ymax = pmax(projected_demand, supply),
                  fill = projected_demand > supply),
              alpha = 0.15) +
  scale_colour_manual(values = c("Projected demand" = "#C00000",
                                  "Supply (flat)"    = "#4472C4")) +
  scale_fill_manual(values = c("FALSE" = "#4472C4", "TRUE" = "#C00000"),
                    guide = "none") +
  scale_x_continuous(breaks = seq(2025, 2036, 2)) +
  facet_wrap(~ muni_name.y, scales = "free_y", ncol = 3) +
  labs(
    title    = "Demand vs supply trajectory — 6 most urgent municipalities",
    subtitle = "Strong ageing scenario, flat supply. Red shading = demand exceeds supply.",
    x = NULL, y = "Long-term institutional users / beds",
    colour = NULL
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom",
        panel.grid.minor = element_blank(),
        strip.text = element_text(face = "bold"))

7 Markov Model !!!!!!!!

# ════════════════════════════════════════════════════════════════════════════
# PART A — MARKOV MODEL ENGINE
# ════════════════════════════════════════════════════════════════════════════

# ── A1: Define the transition matrix ─────────────────────────────────────────
# Rows = state you are IN, Columns = state you move TO
# Each row must sum to 1.0 (you must go somewhere each year)
#
# States:
#   1 = Home care light
#   2 = Home care moderate  
#   3 = Home care intensive
#   4 = Nursing home
#   5 = Dead

build_transition_matrix <- function(
    # Progression probabilities (moving to next home care level)
    p_light_to_moderate   = 0.15,
    p_moderate_to_intense = 0.12,
    
    # Nursing home admission probabilities (from each home care level)
    p_light_to_nh         = 0.02,
    p_moderate_to_nh      = 0.08,
    p_intense_to_nh       = 0.20,
    
    # Annual mortality probabilities (by state)
    p_die_light           = 0.03,
    p_die_moderate        = 0.05,
    p_die_intense         = 0.08,
    p_die_nh              = 0.25
) {
  
  # Build the 5x5 matrix row by row
  # Each row: probabilities of moving to states 1, 2, 3, 4, 5
  
  m <- matrix(0, nrow = 5, ncol = 5,
              dimnames = list(
                from = c("Light", "Moderate", "Intensive", "NH", "Dead"),
                to   = c("Light", "Moderate", "Intensive", "NH", "Dead")
              ))
  
  # Row 1: Light home care
  m[1, 2] <- p_light_to_moderate          # progress to moderate
  m[1, 4] <- p_light_to_nh               # admit to NH directly
  m[1, 5] <- p_die_light                 # die
  m[1, 1] <- 1 - sum(m[1, ])            # stay (remainder)
  
  # Row 2: Moderate home care
  m[2, 3] <- p_moderate_to_intense       # progress to intensive
  m[2, 4] <- p_moderate_to_nh            # admit to NH
  m[2, 5] <- p_die_moderate              # die
  m[2, 2] <- 1 - sum(m[2, ])            # stay
  
  # Row 3: Intensive home care
  m[3, 4] <- p_intense_to_nh             # admit to NH
  m[3, 5] <- p_die_intense               # die
  m[3, 3] <- 1 - sum(m[3, ])            # stay
  
  # Row 4: Nursing home
  m[4, 5] <- p_die_nh                    # die
  m[4, 4] <- 1 - sum(m[4, ])            # stay
  
  # Row 5: Dead (absorbing — once dead, stay dead)
  m[5, 5] <- 1
  
  # Validate: every row must sum to exactly 1
  row_sums <- rowSums(m)
  if (any(abs(row_sums - 1) > 1e-9)) {
    stop("Transition matrix rows do not sum to 1. Check your probabilities.\n",
         "Row sums: ", paste(round(row_sums, 6), collapse = ", "))
  }
  
  # Validate: no negative probabilities
  if (any(m < 0)) {
    stop("Negative probability detected. Your input probabilities sum to more than 1 ",
         "in at least one row. Reduce them.")
  }
  
  return(m)
}
# ── A2: The model engine ──────────────────────────────────────────────────────
# Takes a starting cohort and runs it forward year by year

run_markov <- function(
    transition_matrix,        # 5x5 matrix from build_transition_matrix()
    cohort_start,             # named vector: how many people start in each state
                              # e.g. c(Light=1000, Moderate=500, Intensive=200, NH=300, Dead=0)
    n_years         = 11,     # how many years to run (2025 to 2036 = 11 steps)
    start_year      = 2025
) {
  
  # Storage: one row per year, one column per state
  n_states <- nrow(transition_matrix)
  state_names <- rownames(transition_matrix)
  years <- start_year:(start_year + n_years - 1)
  
  results <- matrix(
    NA,
    nrow = n_years,
    ncol = n_states,
    dimnames = list(year = years, state = state_names)
  )
  
  # Year 1 = starting cohort
  results[1, ] <- cohort_start
  
  # Run forward: multiply current state vector by transition matrix
  for (t in 2:n_years) {
    results[t, ] <- results[t - 1, ] %*% transition_matrix
  }
  
  # Convert to tidy data frame for easy plotting and joining
  results_df <- as.data.frame(results) |>
    tibble::rownames_to_column("year") |>
    dplyr::mutate(year = as.integer(year)) |>
    tidyr::pivot_longer(
      cols      = -year,
      names_to  = "state",
      values_to = "n_people"
    ) |>
    dplyr::mutate(
      state = factor(state,
                     levels = c("Light", "Moderate", "Intensive", "NH", "Dead"))
    )
  
  return(results_df)
}
# ── A3: Helper — extract annual NH admissions ────────────────────────────────
# NH admissions in year t = people who were NOT in NH in year t-1
# but ARE in NH in year t. This is different from total NH residents.

get_nh_admissions <- function(markov_results) {
  markov_results |>
    dplyr::filter(state == "NH") |>
    dplyr::arrange(year) |>
    dplyr::mutate(
      nh_admissions = n_people - dplyr::lag(n_people, default = n_people[1])
    ) |>
    dplyr::select(year, nh_residents = n_people, nh_admissions)
}
# ── A4: Quick test run ───────────────────────────────────────────────────────
# Build a transition matrix with default probabilities
baseline_matrix <- build_transition_matrix()

cat("Transition matrix (baseline):\n")
## Transition matrix (baseline):
print(round(baseline_matrix, 3))
##            to
## from        Light Moderate Intensive   NH Dead
##   Light       0.8     0.15      0.00 0.02 0.03
##   Moderate    0.0     0.75      0.12 0.08 0.05
##   Intensive   0.0     0.00      0.72 0.20 0.08
##   NH          0.0     0.00      0.00 0.75 0.25
##   Dead        0.0     0.00      0.00 0.00 1.00
cat("\nRow sums (all must be 1.0):\n")
## 
## Row sums (all must be 1.0):
print(rowSums(baseline_matrix))
##     Light  Moderate Intensive        NH      Dead 
##         1         1         1         1         1
# Define a starting cohort of 10,000 home care users
# Distribution matches the national need-level shares from your data:
# Light 37%, Moderate 30%, Intensive 25%, NH 8% (already there), Dead 0%
cohort <- c(Light = 3700, Moderate = 3000, Intensive = 2500,
            NH = 800, Dead = 0)

cat("\nStarting cohort:\n")
## 
## Starting cohort:
print(cohort)
##     Light  Moderate Intensive        NH      Dead 
##      3700      3000      2500       800         0
cat("Total:", sum(cohort), "people\n\n")
## Total: 10000 people
# Run the model
baseline_run <- run_markov(
  transition_matrix = baseline_matrix,
  cohort_start      = cohort,
  n_years           = 12,
  start_year        = 2025
)
# Show results
cat("State populations by year:\n")
## State populations by year:
baseline_run |>
  tidyr::pivot_wider(names_from = state, values_from = n_people) |>
  dplyr::mutate(across(where(is.numeric), ~ round(.x))) |>
  kable(caption = "Markov model — cohort state populations 2025-2036 (baseline)") |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Markov model — cohort state populations 2025-2036 (baseline)
year Light Moderate Intensive NH Dead
2025 3700 3000 2500 800 0
2026 2960 2805 2160 1414 661
2027 2368 2548 1892 1776 1416
2028 1894 2266 1668 1962 2210
2029 1516 1984 1473 2024 3004
2030 1212 1715 1298 2002 3773
2031 970 1468 1141 1922 4499
2032 776 1247 997 1807 5173
2033 621 1051 868 1670 5790
2034 497 882 751 1522 6348
2035 397 736 646 1372 6848
2036 318 611 554 1225 7292
# NH admissions
cat("\nNursing home admissions per year:\n")
## 
## Nursing home admissions per year:
get_nh_admissions(baseline_run) |>
  mutate(across(where(is.numeric), ~ comma(round(.x)))) |>
  kable(caption = "Annual NH admissions and total NH residents (baseline cohort)",
        col.names = c("Year", "NH residents", "New admissions")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Annual NH admissions and total NH residents (baseline cohort)
Year NH residents New admissions
2,025 800 0
2,026 1,414 614
2,027 1,776 362
2,028 1,962 186
2,029 2,024 62
2,030 2,002 -22
2,031 1,922 -79
2,032 1,807 -116
2,033 1,670 -137
2,034 1,522 -147
2,035 1,372 -150
2,036 1,225 -147
# Plot
baseline_run |>
  filter(state != "Dead") |>
  ggplot(aes(x = year, y = n_people, fill = state)) +
  geom_area(alpha = 0.8, colour = "white", linewidth = 0.3) +
  scale_fill_manual(values = c(
    "Light"     = "#4472C4",
    "Moderate"  = "#ED7D31",
    "Intensive" = "#FFC000",
    "NH"        = "#C00000"
  )) +
  scale_x_continuous(breaks = seq(2025, 2036, 2)) +
  scale_y_continuous(labels = comma) +
  labs(
    title    = "Cohort flow through care states 2025-2036",
    subtitle = "Baseline transition probabilities — cohort of 10,000 home care users",
    x = NULL, y = "Number of people", fill = "State"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.minor = element_blank())

8 Pat B: Using real life data:

# ════════════════════════════════════════════════════════════════════════════
# PART B — CALIBRATION + OPEN COHORT ENGINE
# ════════════════════════════════════════════════════════════════════════════

# ── B1: Calibration targets (unchanged from before) ──────────────────────────
total_hc_elderly   <- sum(users$home_care_elderly, na.rm = TRUE)
observed_light     <- round(total_hc_elderly * 0.367)
observed_moderate  <- round(total_hc_elderly * 0.298)
observed_intensive <- round(total_hc_elderly * 0.247)
observed_nh        <- sum(users$long_term_stay_in_institutions, na.rm = TRUE)

avg_nh_stay_years          <- 3
observed_nh_admissions_annual <- round(observed_nh / avg_nh_stay_years)

p_die_light_obs     <- 0.030
p_die_moderate_obs  <- 0.050
p_die_intensive_obs <- 0.080
p_die_nh_obs        <- 0.250

cat("=== 2025 CALIBRATION TARGETS ===\n")
## === 2025 CALIBRATION TARGETS ===
cat("Home care — light:           ", comma(observed_light),               "\n")
## Home care — light:            41,597
cat("Home care — moderate:        ", comma(observed_moderate),            "\n")
## Home care — moderate:         33,776
cat("Home care — intensive:       ", comma(observed_intensive),           "\n")
## Home care — intensive:        27,996
cat("NH long-term residents:      ", comma(observed_nh),                  "\n")
## NH long-term residents:       30,622
cat("Estimated annual NH admissions:", comma(observed_nh_admissions_annual), "\n")
## Estimated annual NH admissions: 10,207
# ── B2: Calibration loss function (unchanged) ────────────────────────────────
calibration_loss <- function(params) {
  p_l2m  <- params[1]; p_m2i  <- params[2]
  p_l2nh <- params[3]; p_m2nh <- params[4]; p_i2nh <- params[5]

  if (any(params < 0) || any(params > 1))                  return(1e9)
  if (p_l2m + p_l2nh + p_die_light_obs     > 1)            return(1e9)
  if (p_m2i + p_m2nh + p_die_moderate_obs  > 1)            return(1e9)
  if (p_i2nh + p_die_intensive_obs         > 1)            return(1e9)

  m <- tryCatch(
    build_transition_matrix(
      p_light_to_moderate   = p_l2m,
      p_moderate_to_intense = p_m2i,
      p_light_to_nh         = p_l2nh,
      p_moderate_to_nh      = p_m2nh,
      p_intense_to_nh       = p_i2nh,
      p_die_light           = p_die_light_obs,
      p_die_moderate        = p_die_moderate_obs,
      p_die_intense         = p_die_intensive_obs,
      p_die_nh              = p_die_nh_obs
    ),
    error = function(e) NULL
  )
  if (is.null(m)) return(1e9)

  cohort_2025 <- c(Light = observed_light, Moderate = observed_moderate,
                   Intensive = observed_intensive, NH = observed_nh, Dead = 0)

  result   <- run_markov(m, cohort_2025, n_years = 2, start_year = 2025)
  year2    <- result |> filter(year == 2026) |>
              select(state, n_people) |> tibble::deframe()

  nh_deaths_pred    <- observed_nh * p_die_nh_obs
  nh_admissions_pred <- (year2["NH"] - observed_nh) + nh_deaths_pred

  loss <-
    ((year2["Light"]    - observed_light)    / observed_light)^2    * 1 +
    ((year2["Moderate"] - observed_moderate) / observed_moderate)^2 * 1 +
    ((year2["Intensive"]- observed_intensive)/ observed_intensive)^2* 1 +
    ((nh_admissions_pred - observed_nh_admissions_annual) /
       observed_nh_admissions_annual)^2                             * 3
  return(loss)
}
# ── B3: Run optimiser and build calibrated matrix ────────────────────────────
cat("\nRunning calibration...\n")
## 
## Running calibration...
start_params <- c(p_light_to_moderate   = 0.15,
                  p_moderate_to_intense = 0.12,
                  p_light_to_nh         = 0.02,
                  p_moderate_to_nh      = 0.08,
                  p_intense_to_nh       = 0.20)

calibration_result <- optim(
  par     = start_params,
  fn      = calibration_loss,
  method  = "Nelder-Mead",
  control = list(maxit = 10000, reltol = 1e-10)
)

calibrated_params <- calibration_result$par
names(calibrated_params) <- names(start_params)

cat("Converged:", calibration_result$convergence == 0,
    "| Loss:", round(calibration_result$value, 8), "\n\n")
## Converged: TRUE | Loss: 0.06413883
calibrated_matrix <- build_transition_matrix(
  p_light_to_moderate   = calibrated_params["p_light_to_moderate"],
  p_moderate_to_intense = calibrated_params["p_moderate_to_intense"],
  p_light_to_nh         = calibrated_params["p_light_to_nh"],
  p_moderate_to_nh      = calibrated_params["p_moderate_to_nh"],
  p_intense_to_nh       = calibrated_params["p_intense_to_nh"],
  p_die_light           = p_die_light_obs,
  p_die_moderate        = p_die_moderate_obs,
  p_die_intense         = p_die_intensive_obs,
  p_die_nh              = p_die_nh_obs
)

cat("Calibrated transition matrix:\n")
## Calibrated transition matrix:
print(round(calibrated_matrix, 4))
##            to
## from         Light Moderate Intensive     NH Dead
##   Light     0.8266   0.1154    0.0000 0.0281 0.03
##   Moderate  0.0000   0.7171    0.1318 0.1011 0.05
##   Intensive 0.0000   0.0000    0.7242 0.1958 0.08
##   NH        0.0000   0.0000    0.0000 0.7500 0.25
##   Dead      0.0000   0.0000    0.0000 0.0000 1.00
# ── B4: New entrant projection ───────────────────────────────────────────────
# New entrants = elderly people entering home care for the first time each year.
# Estimated as: growth in elderly population × home care entry rate.
#
# Home care entry rate = share of elderly population entering HC in a year.
# We estimate this from the current stock and average HC duration.
# Average time in home care before NH or death ≈ 4 years (literature estimate).
# Entry rate ≈ current HC users / (elderly population × avg duration)

avg_hc_duration_years <- 4.0

# 2025 total elderly population (from projections, both scenarios identical in 2025)
elderly_pop_2025 <- national_proj |>
  filter(year == 2025, scenario == "Strong ageing") |>
  summarise(total = sum(population)) |>
  pull(total)

# Annual home care entry rate
hc_entry_rate <- total_hc_elderly / (elderly_pop_2025 * avg_hc_duration_years)

cat("\nHome care entry rate (annual probability of entering HC):",
    round(hc_entry_rate, 4), "\n")
## 
## Home care entry rate (annual probability of entering HC): 0.0302
# Project new entrants for each year and scenario
new_entrants_proj <- national_elderly_proj |>
  mutate(
    new_entrants = round(total_elderly * hc_entry_rate),
    # New entrants start in light home care state
    entrants_light     = new_entrants,
    entrants_moderate  = 0,
    entrants_intensive = 0
  )

cat("\nProjected new HC entrants by year (strong ageing):\n")
## 
## Projected new HC entrants by year (strong ageing):
new_entrants_proj |>
  filter(scenario == "Strong ageing") |>
  select(year, total_elderly, new_entrants) |>
  mutate(across(where(is.numeric), comma)) |>
  kable(caption = "Projected annual new home care entrants",
        col.names = c("Year", "Total elderly", "New HC entrants")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Projected annual new home care entrants
Year Total elderly New HC entrants
2,025 938,331 28,336
2,026 961,814 29,045
2,027 985,854 29,771
2,028 1,009,922 30,498
2,029 1,034,215 31,231
2,030 1,058,794 31,973
2,031 1,084,221 32,741
2,032 1,111,425 33,563
2,033 1,139,078 34,398
2,034 1,167,117 35,245
# ── B5: Open cohort Markov engine ────────────────────────────────────────────
# Runs the Markov model with new entrants added each year.

run_markov_open <- function(
    transition_matrix,
    cohort_start,
    new_entrants_df,
    scenario_name,
    start_year = 2025
) {
  years       <- sort(unique(new_entrants_df$year))
  n_years     <- length(years)
  state_names <- rownames(transition_matrix)

  results <- matrix(NA, nrow = n_years, ncol = length(state_names),
                    dimnames = list(year  = as.character(years),
                                    state = state_names))

  results[1, ] <- cohort_start

  entrants <- new_entrants_df |>
    filter(scenario == scenario_name) |>
    arrange(year)

  for (t in 2:n_years) {

    # %*% returns a matrix — drop to a named vector with as.vector()
    transitioned <- as.vector(results[t - 1, ] %*% transition_matrix)
    names(transitioned) <- state_names

    # Add new entrants for this year
    yr      <- years[t]
    ent_row <- entrants |> filter(year == yr)

    if (nrow(ent_row) > 0) {
      transitioned["Light"]     <- transitioned["Light"]     + ent_row$entrants_light
      transitioned["Moderate"]  <- transitioned["Moderate"]  + ent_row$entrants_moderate
      transitioned["Intensive"] <- transitioned["Intensive"] + ent_row$entrants_intensive
    }

    results[t, ] <- transitioned
  }

  as.data.frame(results) |>
    tibble::rownames_to_column("year") |>
    mutate(year = as.integer(year), scenario = scenario_name) |>
    pivot_longer(cols      = -c(year, scenario),
                 names_to  = "state",
                 values_to = "n_people") |>
    mutate(state = factor(state,
                          levels = c("Light","Moderate","Intensive","NH","Dead")))
}
# ── B6: Run open cohort model for both scenarios ─────────────────────────────
cohort_2025 <- c(Light     = observed_light,
                 Moderate  = observed_moderate,
                 Intensive = observed_intensive,
                 NH        = observed_nh,
                 Dead      = 0)

open_strong <- run_markov_open(
  transition_matrix = calibrated_matrix,
  cohort_start      = cohort_2025,
  new_entrants_df   = new_entrants_proj,
  scenario_name     = "Strong ageing",
  start_year        = 2025
)

open_weak <- run_markov_open(
  transition_matrix = calibrated_matrix,
  cohort_start      = cohort_2025,
  new_entrants_df   = new_entrants_proj,
  scenario_name     = "Weak ageing",
  start_year        = 2025
)

open_combined <- bind_rows(open_strong, open_weak)
# ── B7: NH trajectory with supply comparison ─────────────────────────────────
national_supply_flat <- sum(users$institution_all_available_beds, na.rm = TRUE)

nh_open_trajectory <- open_combined |>
  filter(state == "NH") |>
  arrange(scenario, year) |>
  group_by(scenario) |>
  mutate(
    nh_admissions = n_people - lag(n_people, default = first(n_people)),
    supply        = national_supply_flat,
    gap           = n_people - supply,
    over_capacity = gap > 0
  ) |>
  ungroup()

cat("\nNH residents vs supply — open cohort model:\n")
## 
## NH residents vs supply — open cohort model:
nh_open_trajectory |>
  mutate(
    n_people      = comma(round(n_people)),
    nh_admissions = comma(round(nh_admissions)),
    supply        = comma(supply),
    gap           = comma(round(gap))
  ) |>
  select(scenario, year, n_people, nh_admissions, supply, gap, over_capacity) |>
  kable(
    caption   = "Open cohort — NH residents vs supply 2025-2036",
    col.names = c("Scenario", "Year", "NH residents", "Net change",
                  "Supply", "Gap", "Over capacity")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover")) |>
  row_spec(which(nh_open_trajectory$over_capacity), background = "#FCE4D6")
Open cohort — NH residents vs supply 2025-2036
Scenario Year NH residents Net change Supply Gap Over capacity
Strong ageing 2025 30,622 0 41,650 -11,028 FALSE
Strong ageing 2026 33,029 2,407 41,650 -8,622 FALSE
Strong ageing 2027 34,326 1,297 41,650 -7,324 FALSE
Strong ageing 2028 35,149 823 41,650 -6,501 FALSE
Strong ageing 2029 35,929 780 41,650 -5,721 FALSE
Strong ageing 2030 36,927 998 41,650 -4,723 FALSE
Strong ageing 2031 38,275 1,348 41,650 -3,376 FALSE
Strong ageing 2032 40,017 1,742 41,650 -1,633 FALSE
Strong ageing 2033 42,141 2,124 41,650 490 TRUE
Strong ageing 2034 44,599 2,459 41,650 2,949 TRUE
Weak ageing 2025 30,622 0 41,650 -11,028 FALSE
Weak ageing 2026 33,029 2,407 41,650 -8,622 FALSE
Weak ageing 2027 34,321 1,292 41,650 -7,329 FALSE
Weak ageing 2028 35,132 810 41,650 -6,519 FALSE
Weak ageing 2029 35,887 755 41,650 -5,763 FALSE
Weak ageing 2030 36,844 958 41,650 -4,806 FALSE
Weak ageing 2031 38,134 1,290 41,650 -3,516 FALSE
Weak ageing 2032 39,797 1,663 41,650 -1,854 FALSE
Weak ageing 2033 41,817 2,020 41,650 167 TRUE
Weak ageing 2034 44,147 2,330 41,650 2,497 TRUE
# ── B8: Plot ─────────────────────────────────────────────────────────────────
nh_open_trajectory |>
  ggplot(aes(x = year)) +
  geom_line(aes(y = n_people, colour = scenario), linewidth = 1.2) +
  geom_hline(aes(yintercept = national_supply_flat),
             linetype = "dashed", colour = "#4472C4", linewidth = 1) +
  geom_ribbon(
    data = nh_open_trajectory |> filter(over_capacity),
    aes(ymin = supply, ymax = n_people, fill = scenario),
    alpha = 0.2
  ) +
  annotate("text", x = 2026, y = national_supply_flat + 500,
           label = "Supply ceiling (flat)", hjust = 0,
           colour = "#4472C4", size = 3.5) +
  scale_colour_manual(values = c("Strong ageing" = "#C00000",
                                  "Weak ageing"   = "#ED7D31")) +
  scale_fill_manual(values  = c("Strong ageing" = "#C00000",
                                 "Weak ageing"   = "#ED7D31"),
                    guide = "none") +
  scale_x_continuous(breaks = 2025:2036) +
  scale_y_continuous(labels = comma) +
  labs(
    title    = "Projected NH residents vs supply — open cohort Markov model",
    subtitle = "New entrants added annually from population projections. Dashed = flat supply.",
    x = NULL, y = "NH residents / beds", colour = "Ageing scenario"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.minor = element_blank())

8.1 Part C: What if we stabalise?

# ════════════════════════════════════════════════════════════════════════════
# PART C — SCENARIO COMPARISON
# Effect of early-stage stabilisation on nursing home demand
# ════════════════════════════════════════════════════════════════════════════

# ── C1: Intervention logic ───────────────────────────────────────────────────
# The intervention reduces progression probabilities at light and moderate stages.
# This slows the pipeline feeding into intensive care and eventually into NH.
# Effect is parameterised as a percentage reduction in progression probabilities.
# A secondary smaller effect is allowed at moderate-to-intensive and
# intensive-to-NH to reflect partial downstream benefit.
#
# intervention_strength: 0 = no effect, 1 = 100% reduction (impossible in practice)
# Realistic range: 0.10 to 0.40 (10% to 40% reduction in progression rates)

build_intervention_matrix <- function(
    base_matrix,
    intervention_strength,        # primary effect at light stage
    secondary_multiplier = 0.4    # secondary effect = 40% of primary, at moderate stage
) {

  # Extract baseline probabilities from the calibrated matrix
  p_l2m_base  <- base_matrix["Light",    "Moderate"]
  p_m2i_base  <- base_matrix["Moderate", "Intensive"]
  p_l2nh_base <- base_matrix["Light",    "NH"]
  p_m2nh_base <- base_matrix["Moderate", "NH"]
  p_i2nh_base <- base_matrix["Intensive","NH"]

  # Apply reductions
  # Primary effect: slow light → moderate progression
  p_l2m_new  <- p_l2m_base  * (1 - intervention_strength)

  # Secondary effect: slow moderate → intensive progression
  p_m2i_new  <- p_m2i_base  * (1 - intervention_strength * secondary_multiplier)

  # Small tertiary effect at intensive → NH (40% of secondary = 16% of primary)
  p_i2nh_new <- p_i2nh_base * (1 - intervention_strength *
                                  secondary_multiplier * secondary_multiplier)

  # NH admission from light and moderate: minor effect from stabilisation
  p_l2nh_new <- p_l2nh_base * (1 - intervention_strength * 0.2)
  p_m2nh_new <- p_m2nh_base * (1 - intervention_strength * secondary_multiplier * 0.5)

  # Build new matrix with intervention probabilities
  build_transition_matrix(
    p_light_to_moderate   = p_l2m_new,
    p_moderate_to_intense = p_m2i_new,
    p_light_to_nh         = p_l2nh_new,
    p_moderate_to_nh      = p_m2nh_new,
    p_intense_to_nh       = p_i2nh_new,
    p_die_light           = p_die_light_obs,
    p_die_moderate        = p_die_moderate_obs,
    p_die_intense         = p_die_intensive_obs,
    p_die_nh              = p_die_nh_obs
  )
}
# ── C2: Show what the intervention does to transition probabilities ───────────
cat("=== EFFECT OF INTERVENTION ON TRANSITION PROBABILITIES ===\n\n")
## === EFFECT OF INTERVENTION ON TRANSITION PROBABILITIES ===
intervention_levels <- c(0, 0.10, 0.20, 0.30, 0.40)

prob_comparison <- map_dfr(intervention_levels, function(strength) {
  m <- build_intervention_matrix(calibrated_matrix, strength)
  tibble(
    intervention_strength   = paste0(strength * 100, "%"),
    p_light_to_moderate     = round(m["Light",    "Moderate"],  4),
    p_moderate_to_intensive = round(m["Moderate", "Intensive"], 4),
    p_intensive_to_nh       = round(m["Intensive","NH"],        4),
    p_light_to_nh           = round(m["Light",    "NH"],        4),
    p_moderate_to_nh        = round(m["Moderate", "NH"],        4)
  )
})

prob_comparison |>
  kable(
    caption   = "Transition probabilities under each intervention strength",
    col.names = c("Intervention", "Light→Moderate", "Moderate→Intensive",
                  "Intensive→NH", "Light→NH", "Moderate→NH")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Transition probabilities under each intervention strength
Intervention Light→Moderate Moderate→Intensive Intensive→NH Light→NH Moderate→NH
0% 0.1154 0.1318 0.1958 0.0281 0.1011
10% 0.1038 0.1266 0.1927 0.0275 0.0990
20% 0.0923 0.1213 0.1895 0.0269 0.0970
30% 0.0808 0.1160 0.1864 0.0264 0.0950
40% 0.0692 0.1108 0.1833 0.0258 0.0930
# ── C3: Run open cohort model for each intervention level ────────────────────
cat("\nRunning scenarios...\n")
## 
## Running scenarios...
scenarios_all <- map_dfr(intervention_levels, function(strength) {

  int_matrix <- build_intervention_matrix(calibrated_matrix, strength)

  run_markov_open(
    transition_matrix = int_matrix,
    cohort_start      = cohort_2025,
    new_entrants_df   = new_entrants_proj,
    scenario_name     = "Strong ageing",
    start_year        = 2025
  ) |>
    mutate(intervention = paste0(strength * 100, "% reduction"))
})

cat("Done.\n")
## Done.
# ── C4: Extract NH trajectory for each scenario ──────────────────────────────
nh_scenarios <- scenarios_all |>
  filter(state == "NH") |>
  arrange(intervention, year) |>
  group_by(intervention) |>
  mutate(
    nh_admissions = n_people - lag(n_people, default = first(n_people)),
    supply        = national_supply_flat,
    gap           = n_people - supply,
    over_capacity = gap > 0
  ) |>
  ungroup() |>
  mutate(
    intervention = factor(intervention,
                          levels = paste0(intervention_levels * 100, "% reduction"))
  )
# ── C5: Crossover year by intervention strength ──────────────────────────────
crossover_by_intervention <- nh_scenarios |>
  filter(over_capacity) |>
  group_by(intervention) |>
  summarise(crossover_year = min(year), .groups = "drop")

# Add scenarios with no crossover
all_interventions <- tibble(
  intervention = factor(paste0(intervention_levels * 100, "% reduction"),
                        levels = paste0(intervention_levels * 100, "% reduction"))
)

crossover_by_intervention <- all_interventions |>
  left_join(crossover_by_intervention, by = "intervention") |>
  mutate(crossover_year = if_else(is.na(crossover_year),
                                  "No crossover by 2036", as.character(crossover_year)))

cat("\n=== CROSSOVER YEAR BY INTERVENTION STRENGTH ===\n")
## 
## === CROSSOVER YEAR BY INTERVENTION STRENGTH ===
crossover_by_intervention |>
  kable(
    caption   = "First year NH demand exceeds supply — by intervention strength",
    col.names = c("Intervention strength", "Crossover year")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
First year NH demand exceeds supply — by intervention strength
Intervention strength Crossover year
0% reduction 2033
10% reduction 2034
20% reduction No crossover by 2036
30% reduction No crossover by 2036
40% reduction No crossover by 2036
# ── C6: People kept out of nursing homes ─────────────────────────────────────
# Compared to baseline (0% intervention)
baseline_nh <- nh_scenarios |>
  filter(intervention == "0% reduction") |>
  select(year, baseline_residents = n_people)

avoided_admissions <- nh_scenarios |>
  left_join(baseline_nh, by = "year") |>
  mutate(
    residents_avoided = baseline_residents - n_people,
    cumulative_avoided = cumsum(pmax(residents_avoided, 0))
  ) |>
  filter(intervention != "0% reduction") |>
  select(intervention, year, n_people, baseline_residents,
         residents_avoided, cumulative_avoided, over_capacity)

cat("\nPeople kept out of nursing homes vs baseline:\n")
## 
## People kept out of nursing homes vs baseline:
avoided_admissions |>
  filter(year >= 2029) |>
  mutate(
    n_people           = comma(round(n_people)),
    baseline_residents = comma(round(baseline_residents)),
    residents_avoided  = comma(round(residents_avoided)),
    cumulative_avoided = comma(round(cumulative_avoided))
  ) |>
  select(intervention, year, baseline_residents,
         n_people, residents_avoided, cumulative_avoided) |>
  kable(
    caption   = "NH residents avoided vs baseline — from crossover year onwards",
    col.names = c("Intervention", "Year", "Baseline NH residents",
                  "Intervention NH residents", "Avoided per year",
                  "Cumulative avoided")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 11)
NH residents avoided vs baseline — from crossover year onwards
Intervention Year Baseline NH residents Intervention NH residents Avoided per year Cumulative avoided
10% reduction 2029 35,929 35,283 646 1,643
10% reduction 2030 36,927 36,102 824 2,467
10% reduction 2031 38,275 37,259 1,016 3,483
10% reduction 2032 40,017 38,801 1,216 4,699
10% reduction 2033 42,141 40,724 1,416 6,116
10% reduction 2034 44,599 42,989 1,611 7,726
20% reduction 2029 35,929 34,640 1,290 11,005
20% reduction 2030 36,927 35,281 1,646 12,651
20% reduction 2031 38,275 36,242 2,032 14,684
20% reduction 2032 40,017 37,580 2,437 17,121
20% reduction 2033 42,141 39,293 2,847 19,968
20% reduction 2034 44,599 41,352 3,247 23,216
30% reduction 2029 35,929 34,000 1,930 28,125
30% reduction 2030 36,927 34,462 2,465 30,589
30% reduction 2031 38,275 35,227 3,048 33,637
30% reduction 2032 40,017 36,353 3,663 37,301
30% reduction 2033 42,141 37,850 4,291 41,592
30% reduction 2034 44,599 39,690 4,909 46,501
40% reduction 2029 35,929 33,363 2,566 53,033
40% reduction 2030 36,927 33,647 3,280 56,313
40% reduction 2031 38,275 34,213 4,062 60,375
40% reduction 2032 40,017 35,124 4,893 65,268
40% reduction 2033 42,141 36,394 5,746 71,014
40% reduction 2034 44,599 38,005 6,594 77,608
# ── C7: Main trajectory plot ─────────────────────────────────────────────────
nh_scenarios |>
  ggplot(aes(x = year, y = n_people,
             colour = intervention, group = intervention)) +
  geom_line(linewidth = 1.1) +
  geom_hline(yintercept = national_supply_flat,
             linetype = "dashed", colour = "black", linewidth = 0.8) +
  annotate("text", x = 2025.2, y = national_supply_flat + 600,
           label = "Supply ceiling", hjust = 0, size = 3.5, colour = "black") +
  scale_colour_manual(values = c(
    "0% reduction"  = "#C00000",
    "10% reduction" = "#ED7D31",
    "20% reduction" = "#FFC000",
    "30% reduction" = "#70AD47",
    "40% reduction" = "#4472C4"
  )) +
  scale_x_continuous(breaks = 2025:2036) +
  scale_y_continuous(labels = comma,
                     limits = c(NA, max(nh_scenarios$n_people) * 1.05)) +
  labs(
    title    = "NH residents under different intervention strengths",
    subtitle = "Early-stage stabilisation in home care — strong ageing scenario",
    x = NULL, y = "NH residents", colour = "Intervention"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.minor = element_blank())

# ── C8: Crossover postponement bar chart ─────────────────────────────────────
crossover_plot_data <- crossover_by_intervention |>
  mutate(
    crossover_numeric = suppressWarnings(as.integer(crossover_year)),
    label = if_else(
      is.na(crossover_numeric),
      "No crossover\nby 2036",
      as.character(crossover_numeric)
    ),
    # For plotting: use 2037 as a placeholder for "no crossover"
    plot_year = if_else(is.na(crossover_numeric), 2037L, crossover_numeric),
    baseline_year = min(crossover_numeric, na.rm = TRUE),
    years_postponed = plot_year - baseline_year,
    postponed_label = case_when(
      years_postponed == 0  ~ "Baseline",
      plot_year == 2037     ~ "Prevented",
      TRUE                  ~ paste0("+", years_postponed, " yrs")
    ),
    full_label = paste0(label, "\n(", postponed_label, ")")
  )

crossover_plot_data |>
  ggplot(aes(x = intervention, y = plot_year, fill = intervention)) +
  geom_col(width = 0.6, alpha = 0.85) +
  geom_text(aes(label = full_label),
            vjust = -0.3, size = 3.5, fontface = "bold") +
  geom_hline(yintercept = 2036, linetype = "dashed",
             colour = "black", linewidth = 0.7) +
  annotate("text", x = 0.5, y = 2036.3,
           label = "End of projection window (2036)",
           hjust = 0, size = 3, colour = "black") +
  scale_fill_manual(values = c(
    "0% reduction"  = "#C00000",
    "10% reduction" = "#ED7D31",
    "20% reduction" = "#FFC000",
    "30% reduction" = "#70AD47",
    "40% reduction" = "#4472C4"
  )) +
  scale_y_continuous(limits = c(2020, 2040),
                     breaks = seq(2020, 2040, 2)) +
  labs(
    title    = "Crossover year postponed by early-stage intervention",
    subtitle = "Bars reaching 2037 indicate no crossover within the projection window",
    x = "Intervention strength", y = "Crossover year"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none",
        panel.grid.minor = element_blank())

# ════════════════════════════════════════════════════════════════════════════
# C9: BACK-CALIBRATION — fine-grained intervention search
# ════════════════════════════════════════════════════════════════════════════

# ── Step 1: Create crossover_fine ────────────────────────────────────────────
cat("Running fine-grained intervention search...\n")
## Running fine-grained intervention search...
fine_strengths <- seq(0, 1, by = 0.01)

crossover_fine <- map_dfr(fine_strengths, function(strength) {
  m <- tryCatch(
    build_intervention_matrix(calibrated_matrix, strength),
    error = function(e) NULL
  )
  if (is.null(m)) return(tibble(strength = strength,
                                max_nh   = NA_real_,
                                crossover = NA))

  result <- run_markov_open(
    transition_matrix = m,
    cohort_start      = cohort_2025,
    new_entrants_df   = new_entrants_proj,
    scenario_name     = "Strong ageing",
    start_year        = 2025
  )

  nh_max <- result |>
    filter(state == "NH") |>
    summarise(max_nh = max(n_people)) |>
    pull(max_nh)

  tibble(
    strength  = strength,
    max_nh    = round(nh_max),
    crossover = nh_max > national_supply_flat
  )
})

cat("Search complete.", nrow(crossover_fine), "scenarios evaluated.\n\n")
## Search complete. 101 scenarios evaluated.
# ── Step 2: Minimum intervention check ───────────────────────────────────────
if (nrow(crossover_fine |> filter(!crossover, !is.na(crossover))) == 0) {

  cat("No intervention strength tested fully prevents crossover through 2036.\n")
  cat("The new entrant flow alone is sufficient to exceed supply.\n\n")

  best_case <- crossover_fine |>
    filter(!is.na(crossover)) |>
    slice_min(max_nh, n = 1)
  cat("Best case (strength =", paste0(best_case$strength * 100, "%):\n"))
  cat("  Max NH residents:", comma(best_case$max_nh), "\n")
  cat("  Supply ceiling:  ", comma(national_supply_flat), "\n")
  cat("  Remaining gap:   ", comma(best_case$max_nh - national_supply_flat), "\n\n")

} else {

  min_intervention <- crossover_fine |>
    filter(!crossover) |>
    slice_min(strength, n = 1)

  cat("Minimum intervention to prevent crossover through 2036:\n")
  cat("  Strength:        ", paste0(min_intervention$strength * 100, "%"), "\n")
  cat("  Max NH residents:", comma(min_intervention$max_nh), "\n")
  cat("  Supply ceiling:  ", comma(national_supply_flat), "\n\n")

  m_min <- build_intervention_matrix(calibrated_matrix, min_intervention$strength)
  cat("In plain language:\n")
  cat("  Light→Moderate:    from",
      round(calibrated_matrix["Light",    "Moderate"],  4), "to",
      round(m_min["Light",    "Moderate"],  4), "\n")
  cat("  Moderate→Intensive: from",
      round(calibrated_matrix["Moderate", "Intensive"], 4), "to",
      round(m_min["Moderate", "Intensive"], 4), "\n")
  cat("  Intensive→NH:       from",
      round(calibrated_matrix["Intensive","NH"],        4), "to",
      round(m_min["Intensive","NH"],        4), "\n\n")
}
## Minimum intervention to prevent crossover through 2036:
##   Strength:         19% 
##   Max NH residents: 41,517 
##   Supply ceiling:   41,650 
## 
## In plain language:
##   Light→Moderate:    from 0.1154 to 0.0934 
##   Moderate→Intensive: from 0.1318 to 0.1218 
##   Intensive→NH:       from 0.1958 to 0.1898
# ── Step 3: Gap closure table ─────────────────────────────────────────────────
cat("Gap between max NH residents and supply ceiling by intervention strength:\n")
## Gap between max NH residents and supply ceiling by intervention strength:
crossover_fine |>
  filter(strength %in% seq(0, 1, by = 0.10)) |>
  select(strength, max_nh) |>
  mutate(
    gap            = max_nh - national_supply_flat,
    gap_closed_pct = (1 - gap / first(gap)) * 100
  ) |>
  mutate(
    strength       = paste0(strength * 100, "%"),
    max_nh         = comma(max_nh),
    gap            = comma(gap),
    gap_closed_pct = paste0(round(gap_closed_pct, 1), "%")
  ) |>
  kable(
    caption   = "Gap reduction by intervention strength (strong ageing, flat supply)",
    col.names = c("Intervention", "Max NH residents",
                  "Gap vs supply", "Gap closed vs baseline")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Gap reduction by intervention strength (strong ageing, flat supply)
Intervention Max NH residents Gap vs supply Gap closed vs baseline
0% 44,599 2,949 0%
10% 42,989 1,339 54.6%
20% 41,352 -298 110.1%
40% 38,005 -3,645 223.6%
50% 36,298 -5,352 281.5%
70% 32,823 -8,827 399.4%
80% 31,689 -9,961 437.8%
90% 31,415 -10,235 447.1%
100% 31,236 -10,414 453.2%
# ── Step 4: Gap closure plot ──────────────────────────────────────────────────
crossover_fine |>
  mutate(
    gap          = max_nh - national_supply_flat,
    baseline_gap = first(gap)
  ) |>
  ggplot(aes(x = strength * 100, y = gap)) +
  geom_line(colour = "#4472C4", linewidth = 1.2) +
  geom_hline(yintercept = 0, linetype = "dashed",
             colour = "#C00000", linewidth = 0.8) +
  annotate("text", x = 5, y = 200,
           label = "Supply ceiling", hjust = 0,
           colour = "#C00000", size = 3.5) +
  scale_x_continuous(breaks = seq(0, 100, 10),
                     labels = paste0(seq(0, 100, 10), "%")) +
  scale_y_continuous(labels = comma) +
  labs(
    title    = "Gap between NH demand and supply — by intervention strength",
    subtitle = "Even at 100% intervention the gap is nearly closed but not fully eliminated",
    x        = "Intervention strength (reduction in progression rates)",
    y        = "Remaining gap (NH residents above supply ceiling)"
  ) +
  theme_minimal(base_size = 13) +
  theme(panel.grid.minor = element_blank())

# ── Step 5: Combined intervention + supply expansion ─────────────────────────
cat("\n=== COMBINED INTERVENTION + SUPPLY EXPANSION ===\n")
## 
## === COMBINED INTERVENTION + SUPPLY EXPANSION ===
crossover_fine |>
  filter(strength %in% seq(0, 1, by = 0.10)) |>
  select(strength, max_nh) |>
  mutate(
    beds_needed        = pmax(max_nh - national_supply_flat, 0),
    pct_of_current     = beds_needed / national_supply_flat * 100,
    annual_beds_needed = round(beds_needed / 11)
  ) |>
  mutate(
    strength           = paste0(strength * 100, "%"),
    max_nh             = comma(round(max_nh)),
    beds_needed        = comma(beds_needed),
    pct_of_current     = paste0(round(pct_of_current, 1), "%"),
    annual_beds_needed = comma(annual_beds_needed)
  ) |>
  kable(
    caption   = "Additional beds needed alongside each intervention level",
    col.names = c("Intervention", "Max NH residents", "Additional beds needed",
                  "% of current supply", "Avg beds/year needed")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Additional beds needed alongside each intervention level
Intervention Max NH residents Additional beds needed % of current supply Avg beds/year needed
0% 44,599 2,949 7.1% 268
10% 42,989 1,339 3.2% 122
20% 41,352 0 0% 0
40% 38,005 0 0% 0
50% 36,298 0 0% 0
70% 32,823 0 0% 0
80% 31,689 0 0% 0
90% 31,415 0 0% 0
100% 31,236 0 0% 0
# ── Step 6: Commercial argument ───────────────────────────────────────────────
cat("\n=== THE COMMERCIAL ARGUMENT IN PLAIN NUMBERS ===\n\n")
## 
## === THE COMMERCIAL ARGUMENT IN PLAIN NUMBERS ===
for (pct in c(0, 10, 20, 30, 40)) {
  row <- crossover_fine |> filter(strength == pct / 100)
  if (nrow(row) == 0) next
  gap <- row$max_nh - national_supply_flat
  cat(paste0("  ", pct, "% intervention:  max NH = ", comma(row$max_nh),
             ",  gap = ", comma(round(gap)),
             " beds  (",
             round((1 - gap / (crossover_fine$max_nh[1] - national_supply_flat)) * 100, 1),
             "% of gap closed)\n"))
}
##   0% intervention:  max NH = 44,599,  gap = 2,949 beds  (0% of gap closed)
##   10% intervention:  max NH = 42,989,  gap = 1,339 beds  (54.6% of gap closed)
##   20% intervention:  max NH = 41,352,  gap = -298 beds  (110.1% of gap closed)
##   30% intervention:  max NH = 39,690,  gap = -1,960 beds  (166.5% of gap closed)
##   40% intervention:  max NH = 38,005,  gap = -3,645 beds  (223.6% of gap closed)

8.2 Adding costs

# ════════════════════════════════════════════════════════════════════════════
# PART D — COST INTEGRATION
# ════════════════════════════════════════════════════════════════════════════

# ── D1: Define unit costs ─────────────────────────────────────────────────────
unit_costs <- c(
  Light     = 67921,      # Small need of assistance
  Moderate  = 180340,     # Average-to-much need
  Intensive = 657041,     # Extensive need
  NH        = 1585357,    # Nursing home placement
  Dead      = 0           # No cost
)

# Co-located housing cost (between intensive home care and NH)
# This is not a Markov state in our model but used as a reference benchmark
cost_collocated <- 1238172

cat("=== UNIT COSTS PER STATE (NOK/person/year) ===\n")
## === UNIT COSTS PER STATE (NOK/person/year) ===
tibble(
  State         = names(unit_costs),
  Cost_NOK      = unit_costs,
  Cost_formatted = scales::comma(unit_costs)
) |>
  kable(caption = "Unit costs by care state",
        col.names = c("State", "NOK/person/year", "Formatted")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Unit costs by care state
State NOK/person/year Formatted
Light 67921 67,921
Moderate 180340 180,340
Intensive 657041 657,041
NH 1585357 1,585,357
Dead 0 0
# ── D2: Cost function — apply unit costs to Markov output ────────────────────
apply_costs <- function(markov_df) {
  markov_df |>
    mutate(
      unit_cost    = unit_costs[as.character(state)],
      total_cost   = n_people * unit_cost
    )
}
# ── D3: Total annual system cost — baseline vs intervention scenarios ─────────
# Run cost calculation for all intervention scenarios from Part C
costed_scenarios <- scenarios_all |>
  apply_costs() |>
  group_by(intervention, year) |>
  summarise(
    total_cost_NOK    = sum(total_cost, na.rm = TRUE),
    total_people      = sum(n_people[state != "Dead"], na.rm = TRUE),
    nh_residents      = n_people[state == "NH"],
    .groups = "drop"
  ) |>
  mutate(
    total_cost_MNOK   = total_cost_NOK / 1e6,
    intervention      = factor(intervention,
                               levels = paste0(c(0,10,20,30,40), "% reduction"))
  )

cat("\nTotal annual system cost by scenario (NOK million):\n")
## 
## Total annual system cost by scenario (NOK million):
costed_scenarios |>
  select(intervention, year, total_cost_MNOK) |>
  pivot_wider(names_from = intervention, values_from = total_cost_MNOK) |>
  mutate(across(where(is.numeric), ~ round(.x, 0))) |>
  mutate(across(where(is.numeric), comma)) |>
  kable(caption = "Total annual system cost — all states (NOK million)",
        col.names = c("Year", "0% reduction", "10% reduction",
                      "20% reduction", "30% reduction", "40% reduction")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Total annual system cost — all states (NOK million)
Year 0% reduction 10% reduction 20% reduction 30% reduction 40% reduction
2,025 75,858 75,858 75,858 75,858 75,858
2,026 78,151 77,800 77,448 77,096 76,745
2,027 79,355 78,681 78,007 77,335 76,663
2,028 80,537 79,522 78,509 77,497 76,487
2,029 82,315 80,926 79,535 78,145 76,754
2,030 84,985 83,190 81,386 79,576 77,760
2,031 88,629 86,409 84,168 81,907 79,627
2,032 93,198 90,552 87,864 85,135 82,368
2,033 98,568 95,511 92,384 89,188 85,926
2,034 104,584 101,144 97,599 93,952 90,201
# ── D4: Cost savings vs baseline ─────────────────────────────────────────────
baseline_cost <- costed_scenarios |>
  filter(intervention == "0% reduction") |>
  select(year, baseline_cost = total_cost_NOK)

cost_savings <- costed_scenarios |>
  left_join(baseline_cost, by = "year") |>
  mutate(
    annual_saving_NOK  = baseline_cost - total_cost_NOK,
    annual_saving_MNOK = annual_saving_NOK / 1e6
  ) |>
  filter(intervention != "0% reduction")

cat("\nAnnual cost savings vs baseline (NOK million):\n")
## 
## Annual cost savings vs baseline (NOK million):
cost_savings |>
  select(intervention, year, annual_saving_MNOK) |>
  pivot_wider(names_from = intervention, values_from = annual_saving_MNOK) |>
  mutate(across(where(is.numeric), ~ round(.x, 1))) |>
  mutate(across(where(is.numeric), comma)) |>
  kable(caption = "Annual cost savings vs baseline (NOK million)",
        col.names = c("Year", "10% reduction", "20% reduction",
                      "30% reduction", "40% reduction")) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Annual cost savings vs baseline (NOK million)
Year 10% reduction 20% reduction 30% reduction 40% reduction
2,025 0 0 0 0
2,026 352 703 1,055 1,406
2,027 674 1,348 2,020 2,692
2,028 1,014 2,028 3,039 4,050
2,029 1,389 2,779 4,170 5,560
2,030 1,795 3,598 5,409 7,225
2,031 2,220 4,461 6,722 9,002
2,032 2,646 5,334 8,063 10,830
2,033 3,058 6,185 9,380 12,642
2,034 3,441 6,985 10,632 14,383
# ── D5: Cumulative cost savings 2025-2034 ────────────────────────────────────
cumulative_savings <- cost_savings |>
  group_by(intervention) |>
  summarise(
    cumulative_saving_BNOK = sum(annual_saving_NOK, na.rm = TRUE) / 1e9,
    avg_annual_saving_MNOK = mean(annual_saving_NOK, na.rm = TRUE) / 1e6,
    saving_by_2034_MNOK    = annual_saving_NOK[year == 2034] / 1e6,
    .groups = "drop"
  )

cat("\nCumulative cost savings 2025-2034:\n")
## 
## Cumulative cost savings 2025-2034:
cumulative_savings |>
  mutate(across(where(is.numeric), ~ round(.x, 1))) |>
  kable(
    caption   = "Cumulative cost savings 2025-2034 by intervention strength",
    col.names = c("Intervention", "Cumulative saving (NOK billion)",
                  "Average annual saving (NOK million)",
                  "Annual saving by 2034 (NOK million)")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Cumulative cost savings 2025-2034 by intervention strength
Intervention Cumulative saving (NOK billion) Average annual saving (NOK million) Annual saving by 2034 (NOK million)
10% reduction 16.6 1658.8 3440.6
20% reduction 33.4 3342.0 6984.8
30% reduction 50.5 5049.0 10632.4
40% reduction 67.8 6779.1 14382.9
# ── D6: Cost per state breakdown — where does the money go? ──────────────────
cost_by_state <- scenarios_all |>
  apply_costs() |>
  filter(intervention %in% c("0% reduction", "30% reduction"),
         state != "Dead") |>
  group_by(intervention, year, state) |>
  summarise(total_cost_MNOK = sum(total_cost, na.rm = TRUE) / 1e6,
            .groups = "drop")

cost_by_state |>
  filter(year %in% c(2025, 2029, 2034)) |>
  mutate(total_cost_MNOK = round(total_cost_MNOK, 0)) |>
  pivot_wider(names_from = year, values_from = total_cost_MNOK) |>
  arrange(intervention, state) |>
  kable(
    caption   = "Cost breakdown by state — baseline vs 30% intervention (NOK million)",
    col.names = c("Intervention", "State", "2025", "2029", "2034")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Cost breakdown by state — baseline vs 30% intervention (NOK million)
Intervention State 2025 2029 2034
0% reduction Light 2825 7648 11103
0% reduction Moderate 6091 5883 9557
0% reduction Intensive 18395 11823 13218
0% reduction NH 48547 56961 70706
30% reduction Light 2825 8248 12684
30% reduction Moderate 6091 5023 7955
30% reduction Intensive 18395 10972 10389
30% reduction NH 48547 53902 62923
# ── D7: NH cost specifically — the avoided bed-cost argument ─────────────────
nh_cost_comparison <- costed_scenarios |>
  mutate(nh_cost_MNOK = nh_residents * unit_costs["NH"] / 1e6) |>
  select(intervention, year, nh_residents, nh_cost_MNOK)

baseline_nh_cost <- nh_cost_comparison |>
  filter(intervention == "0% reduction") |>
  select(year, baseline_nh_cost = nh_cost_MNOK,
         baseline_residents = nh_residents)

nh_savings <- nh_cost_comparison |>
  left_join(baseline_nh_cost, by = "year") |>
  mutate(
    nh_cost_saving_MNOK      = baseline_nh_cost - nh_cost_MNOK,
    residents_avoided        = baseline_residents - nh_residents
  ) |>
  filter(intervention != "0% reduction", year >= 2029)

cat("\nNH cost savings specifically (NOK million) — from crossover year:\n")
## 
## NH cost savings specifically (NOK million) — from crossover year:
nh_savings |>
  select(intervention, year, residents_avoided, nh_cost_saving_MNOK) |>
  mutate(nh_cost_saving_MNOK = round(nh_cost_saving_MNOK, 0),
         residents_avoided   = round(residents_avoided)) |>
  kable(
    caption   = "NH cost savings vs baseline — from 2029 onwards",
    col.names = c("Intervention", "Year", "Residents avoided", "NH cost saving (M NOK)")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 11)
NH cost savings vs baseline — from 2029 onwards
Intervention Year Residents avoided NH cost saving (M NOK)
10% reduction 2029 646 1025
10% reduction 2030 824 1307
10% reduction 2031 1016 1611
10% reduction 2032 1216 1928
10% reduction 2033 1416 2246
10% reduction 2034 1611 2553
20% reduction 2029 1290 2045
20% reduction 2030 1646 2610
20% reduction 2031 2032 3222
20% reduction 2032 2437 3864
20% reduction 2033 2847 4514
20% reduction 2034 3247 5148
30% reduction 2029 1930 3059
30% reduction 2030 2465 3908
30% reduction 2031 3048 4832
30% reduction 2032 3663 5808
30% reduction 2033 4291 6803
30% reduction 2034 4909 7783
40% reduction 2029 2566 4068
40% reduction 2030 3280 5200
40% reduction 2031 4062 6440
40% reduction 2032 4893 7757
40% reduction 2033 5746 9110
40% reduction 2034 6594 10455
# ── D8: The commercial number — cost per avoided NH admission ─────────────────
# If an intervention achieves 30% stabilisation, what is the cost saving
# per person kept out of a nursing home per year?
# This is the number to put in front of a municipality.

cat("\n=== THE COMMERCIAL ARGUMENT IN COST TERMS ===\n\n")
## 
## === THE COMMERCIAL ARGUMENT IN COST TERMS ===
for (int_level in c("10% reduction", "20% reduction", "30% reduction")) {

  total_avoided <- nh_savings |>
    filter(intervention == int_level) |>
    summarise(
      total_residents_avoided   = sum(residents_avoided),
      total_nh_saving_MNOK      = sum(nh_cost_saving_MNOK)
    )

  total_system_saving <- cost_savings |>
    filter(intervention == int_level) |>
    summarise(total = sum(annual_saving_NOK)) |>
    pull(total)

  cat("Intervention:", int_level, "\n")
  cat("  NH residents avoided (2029-2034):    ",
      comma(round(total_avoided$total_residents_avoided)), "\n")
  cat("  NH cost saving (2029-2034):          NOK",
      comma(round(total_avoided$total_nh_saving_MNOK)), "million\n")
  cat("  Total system cost saving (all years): NOK",
      comma(round(total_system_saving / 1e6)), "million\n")
  cat("  Cost saving per avoided NH resident:  NOK",
      comma(round(total_avoided$total_nh_saving_MNOK * 1e6 /
                    max(total_avoided$total_residents_avoided, 1))), "\n\n")
}
## Intervention: 10% reduction 
##   NH residents avoided (2029-2034):     6,730 
##   NH cost saving (2029-2034):          NOK 10,669 million
##   Total system cost saving (all years): NOK 16,588 million
##   Cost saving per avoided NH resident:  NOK 1,585,357 
## 
## Intervention: 20% reduction 
##   NH residents avoided (2029-2034):     13,500 
##   NH cost saving (2029-2034):          NOK 21,403 million
##   Total system cost saving (all years): NOK 33,420 million
##   Cost saving per avoided NH resident:  NOK 1,585,357 
## 
## Intervention: 30% reduction 
##   NH residents avoided (2029-2034):     20,306 
##   NH cost saving (2029-2034):          NOK 32,192 million
##   Total system cost saving (all years): NOK 50,490 million
##   Cost saving per avoided NH resident:  NOK 1,585,357
# ── D9: Visualisation — total system cost trajectories ───────────────────────
costed_scenarios |>
  ggplot(aes(x = year, y = total_cost_MNOK,
             colour = intervention, group = intervention)) +
  geom_line(linewidth = 1.1) +
  scale_colour_manual(values = c(
    "0% reduction"  = "#C00000",
    "10% reduction" = "#ED7D31",
    "20% reduction" = "#FFC000",
    "30% reduction" = "#70AD47",
    "40% reduction" = "#4472C4"
  )) +
  scale_x_continuous(breaks = 2025:2034) +
  scale_y_continuous(labels = ~ paste0(comma(.x / 1000), "B")) +
  labs(
    title    = "Total system care cost — by intervention strength",
    subtitle = "All home care states + nursing home. Strong ageing scenario.",
    x = NULL, y = "Total cost (NOK billion)", colour = "Intervention"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.minor = element_blank())

# ── D10: Visualisation — annual cost saving vs baseline ──────────────────────
cost_savings |>
  ggplot(aes(x = year, y = annual_saving_MNOK,
             colour = intervention, group = intervention)) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 2) +
  scale_colour_manual(values = c(
    "10% reduction" = "#ED7D31",
    "20% reduction" = "#FFC000",
    "30% reduction" = "#70AD47",
    "40% reduction" = "#4472C4"
  )) +
  scale_x_continuous(breaks = 2025:2034) +
  scale_y_continuous(labels = comma) +
  labs(
    title    = "Annual cost saving vs baseline — by intervention strength",
    subtitle = "Saving grows over time as intervention effect compounds through the pipeline",
    x = NULL, y = "Annual saving (NOK million)", colour = "Intervention"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.minor = element_blank())

8.3 Sensitivity Analysis:

# ════════════════════════════════════════════════════════════════════════════
# PART E — SENSITIVITY ANALYSIS
# How much do results change if transition probabilities are higher or lower?
# ════════════════════════════════════════════════════════════════════════════

# ── E1: Define sensitivity scenarios ─────────────────────────────────────────
# We vary all transition probabilities simultaneously by a percentage
# Low  = -20% (optimistic — slower progression than calibrated)
# Base =   0% (calibrated values)
# High = +20% (pessimistic — faster progression than calibrated)
# 
# We also test individual parameter uncertainty for the three most
# important transitions: light→moderate, moderate→intensive, intensive→NH

sensitivity_multipliers <- c(
  "Low (-20%)"  = 0.80,
  "Base (0%)"   = 1.00,
  "High (+20%)" = 1.20
)
# ── E2: Build sensitivity matrices ───────────────────────────────────────────
build_sensitivity_matrix <- function(multiplier) {

  # Scale all progression and NH admission probabilities
  # Keep mortality fixed — these come from life tables not calibration
  p_l2m_new  <- calibrated_matrix["Light",    "Moderate"]  * multiplier
  p_m2i_new  <- calibrated_matrix["Moderate", "Intensive"] * multiplier
  p_l2nh_new <- calibrated_matrix["Light",    "NH"]        * multiplier
  p_m2nh_new <- calibrated_matrix["Moderate", "NH"]        * multiplier
  p_i2nh_new <- calibrated_matrix["Intensive","NH"]        * multiplier

  # Validate: probabilities cannot exceed row constraints
  # If scaling up pushes a row over 1, cap at 95% of the headroom
  cap <- function(p, max_val) min(p, max_val * 0.95)

  headroom_light    <- 1 - p_die_light_obs
  headroom_moderate <- 1 - p_die_moderate_obs
  headroom_intense  <- 1 - p_die_intensive_obs

  p_l2m_new  <- cap(p_l2m_new,  headroom_light    - p_l2nh_new)
  p_m2i_new  <- cap(p_m2i_new,  headroom_moderate - p_m2nh_new)
  p_i2nh_new <- cap(p_i2nh_new, headroom_intense)

  build_transition_matrix(
    p_light_to_moderate   = p_l2m_new,
    p_moderate_to_intense = p_m2i_new,
    p_light_to_nh         = p_l2nh_new,
    p_moderate_to_nh      = p_m2nh_new,
    p_intense_to_nh       = p_i2nh_new,
    p_die_light           = p_die_light_obs,
    p_die_moderate        = p_die_moderate_obs,
    p_die_intense         = p_die_intensive_obs,
    p_die_nh              = p_die_nh_obs
  )
}
# Show what the sensitivity scenarios do to key probabilities
cat("=== SENSITIVITY SCENARIO TRANSITION PROBABILITIES ===\n")
## === SENSITIVITY SCENARIO TRANSITION PROBABILITIES ===
map_dfr(names(sensitivity_multipliers), function(label) {
  mult <- sensitivity_multipliers[label]
  m    <- build_sensitivity_matrix(mult)
  tibble(
    scenario              = label,
    light_to_moderate     = round(m["Light",    "Moderate"],  4),
    moderate_to_intensive = round(m["Moderate", "Intensive"], 4),
    intensive_to_nh       = round(m["Intensive","NH"],        4)
  )
}) |>
  kable(
    caption   = "Key transition probabilities under each sensitivity scenario",
    col.names = c("Scenario", "Light→Moderate",
                  "Moderate→Intensive", "Intensive→NH")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Key transition probabilities under each sensitivity scenario
Scenario Light→Moderate Moderate→Intensive Intensive→NH
Low (-20%) 0.0923 0.1055 0.1566
Base (0%) 0.1154 0.1318 0.1958
High (+20%) 0.1384 0.1582 0.2350
# ── E3: Run open cohort model for each sensitivity scenario ──────────────────
cat("\nRunning sensitivity scenarios...\n")
## 
## Running sensitivity scenarios...
sensitivity_runs <- map_dfr(names(sensitivity_multipliers), function(label) {
  mult <- sensitivity_multipliers[label]
  m    <- build_sensitivity_matrix(mult)

  run_markov_open(
    transition_matrix = m,
    cohort_start      = cohort_2025,
    new_entrants_df   = new_entrants_proj,
    scenario_name     = "Strong ageing",
    start_year        = 2025
  ) |>
    mutate(sensitivity = label)
})

cat("Done.\n")
## Done.
# ── E4: NH trajectory under each sensitivity scenario ────────────────────────
nh_sensitivity <- sensitivity_runs |>
  filter(state == "NH") |>
  arrange(sensitivity, year) |>
  group_by(sensitivity) |>
  mutate(
    nh_admissions = n_people - lag(n_people, default = first(n_people)),
    supply        = national_supply_flat,
    gap           = n_people - supply,
    over_capacity = gap > 0
  ) |>
  ungroup() |>
  mutate(sensitivity = factor(sensitivity,
                              levels = names(sensitivity_multipliers)))

# Crossover year by sensitivity scenario
crossover_sensitivity <- nh_sensitivity |>
  filter(over_capacity) |>
  group_by(sensitivity) |>
  summarise(crossover_year = min(year), .groups = "drop")

cat("\nCrossover year by sensitivity scenario:\n")
## 
## Crossover year by sensitivity scenario:
crossover_sensitivity |>
  kable(
    caption   = "Crossover year — sensitivity analysis",
    col.names = c("Scenario", "Crossover year")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Crossover year — sensitivity analysis
Scenario Crossover year
Base (0%) 2033
High (+20%) 2030
# ── E5: NH residents table — sensitivity scenarios ───────────────────────────
nh_sensitivity |>
  select(sensitivity, year, n_people, gap) |>
  mutate(
    n_people = comma(round(n_people)),
    gap      = comma(round(gap))
  ) |>
  pivot_wider(names_from  = sensitivity,
              values_from = c(n_people, gap),
              names_glue  = "{sensitivity}_{.value}") |>
  select(year,
         starts_with("Low"),
         starts_with("Base"),
         starts_with("High")) |>
  kable(
    caption   = "NH residents and gap vs supply — sensitivity scenarios",
    col.names = c("Year",
                  "Low: residents", "Low: gap",
                  "Base: residents", "Base: gap",
                  "High: residents", "High: gap")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 11)
NH residents and gap vs supply — sensitivity scenarios
Year Low: residents Low: gap Base: residents Base: gap High: residents High: gap
2025 30,622 -11,028 30,622 -11,028 30,622 -11,028
2026 31,016 -10,634 33,029 -8,622 35,041 -6,609
2027 31,014 -10,636 34,326 -7,324 37,583 -4,067
2028 30,953 -10,698 35,149 -6,501 39,220 -2,430
2029 31,054 -10,596 35,929 -5,721 40,615 -1,036
2030 31,451 -10,200 36,927 -4,723 42,164 514
2031 32,206 -9,444 38,275 -3,376 44,064 2,413
2032 33,336 -8,314 40,017 -1,633 46,374 4,723
2033 34,824 -6,826 42,141 490 49,073 7,422
2034 36,635 -5,015 44,599 2,949 52,096 10,446
# ── E6: Cost sensitivity ──────────────────────────────────────────────────────
cost_sensitivity <- sensitivity_runs |>
  mutate(
    unit_cost  = unit_costs[as.character(state)],
    total_cost = n_people * unit_cost
  ) |>
  group_by(sensitivity, year) |>
  summarise(
    total_cost_BNOK = sum(total_cost, na.rm = TRUE) / 1e9,
    nh_residents    = sum(n_people[state == "NH"], na.rm = TRUE),
    .groups = "drop"
  ) |>
  mutate(sensitivity = factor(sensitivity,
                              levels = names(sensitivity_multipliers)))

# Baseline cost for reference
base_cost <- cost_sensitivity |>
  filter(sensitivity == "Base (0%)") |>
  select(year, base_cost = total_cost_BNOK)

cost_sensitivity <- cost_sensitivity |>
  left_join(base_cost, by = "year") |>
  mutate(cost_vs_base_MNOK = (total_cost_BNOK - base_cost) * 1000)

cat("\nTotal system cost by sensitivity scenario (NOK billion):\n")
## 
## Total system cost by sensitivity scenario (NOK billion):
cost_sensitivity |>
  select(sensitivity, year, total_cost_BNOK) |>
  mutate(total_cost_BNOK = round(total_cost_BNOK, 2)) |>
  pivot_wider(names_from = sensitivity, values_from = total_cost_BNOK) |>
  kable(
    caption   = "Total system cost — sensitivity scenarios (NOK billion)",
    col.names = c("Year", "Low (-20%)", "Base (0%)", "High (+20%)")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Total system cost — sensitivity scenarios (NOK billion)
Year Low (-20%) Base (0%) High (+20%)
2025 75.86 75.86 75.86
2026 78.15 81.01 75.29
2027 79.35 83.96 74.66
2028 80.54 86.34 74.53
2029 82.31 89.12 75.21
2030 84.98 92.75 76.83
2031 88.63 97.35 79.41
2032 93.20 102.87 82.89
2033 98.57 109.14 87.18
2034 104.58 115.99 92.14
# ── E7: Individual parameter sensitivity — tornado analysis ──────────────────
# Test each transition probability individually:
# what happens if just one parameter is 20% higher/lower?

cat("\nRunning tornado analysis (individual parameter sensitivity)...\n")
## 
## Running tornado analysis (individual parameter sensitivity)...
params_to_test <- list(
  "Light→Moderate"    = "p_light_to_moderate",
  "Moderate→Intensive"= "p_moderate_to_intense",
  "Intensive→NH"      = "p_intense_to_nh",
  "Light→NH"          = "p_light_to_nh",
  "Moderate→NH"       = "p_moderate_to_nh"
)

base_values <- list(
  p_light_to_moderate   = calibrated_matrix["Light",    "Moderate"],
  p_moderate_to_intense = calibrated_matrix["Moderate", "Intensive"],
  p_intense_to_nh       = calibrated_matrix["Intensive","NH"],
  p_light_to_nh         = calibrated_matrix["Light",    "NH"],
  p_moderate_to_nh      = calibrated_matrix["Moderate", "NH"]
)

tornado_results <- map_dfr(names(params_to_test), function(param_label) {
  param_name <- params_to_test[[param_label]]

  map_dfr(c(low = 0.80, high = 1.20), function(mult) {
    # Build a modified parameter list
    p <- base_values
    p[[param_name]] <- p[[param_name]] * mult

    m <- tryCatch(
      build_transition_matrix(
        p_light_to_moderate   = p$p_light_to_moderate,
        p_moderate_to_intense = p$p_moderate_to_intense,
        p_light_to_nh         = p$p_light_to_nh,
        p_moderate_to_nh      = p$p_moderate_to_nh,
        p_intense_to_nh       = p$p_intense_to_nh,
        p_die_light           = p_die_light_obs,
        p_die_moderate        = p_die_moderate_obs,
        p_die_intense         = p_die_intensive_obs,
        p_die_nh              = p_die_nh_obs
      ),
      error = function(e) NULL
    )
    if (is.null(m)) return(tibble())

    result <- run_markov_open(
      transition_matrix = m,
      cohort_start      = cohort_2025,
      new_entrants_df   = new_entrants_proj,
      scenario_name     = "Strong ageing",
      start_year        = 2025
    )

    max_nh <- result |>
      filter(state == "NH") |>
      summarise(max_nh = max(n_people)) |>
      pull(max_nh)

    tibble(
      parameter  = param_label,
      direction  = if_else(mult < 1, "Low (-20%)", "High (+20%)"),
      max_nh     = round(max_nh),
      mult       = mult
    )
  }, .id = "direction_id")
})

# Add baseline for comparison
base_max_nh <- sensitivity_runs |>
  filter(sensitivity == "Base (0%)", state == "NH") |>
  summarise(max_nh = max(n_people)) |>
  pull(max_nh)

tornado_results <- tornado_results |>
  mutate(
    base_max_nh  = round(base_max_nh),
    diff_from_base = max_nh - base_max_nh
  )

cat("\nTornado analysis — effect of individual parameter variation on max NH residents:\n")
## 
## Tornado analysis — effect of individual parameter variation on max NH residents:
tornado_results |>
  select(parameter, direction, max_nh, diff_from_base) |>
  arrange(parameter, direction) |>
  mutate(
    max_nh         = comma(max_nh),
    diff_from_base = paste0(if_else(diff_from_base > 0, "+", ""),
                            comma(diff_from_base))
  ) |>
  kable(
    caption   = paste("Tornado analysis — baseline max NH residents:",
                      comma(round(base_max_nh))),
    col.names = c("Parameter", "Direction",
                  "Max NH residents", "Difference from base")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Tornado analysis — baseline max NH residents: 44,599
Parameter Direction Max NH residents Difference from base
Intensive→NH High (+20%) 45,549 +950
Intensive→NH Low (-20%) 43,305 -1,294
Light→Moderate High (+20%) 46,512 +1,913
Light→Moderate Low (-20%) 42,490 -2,109
Light→NH High (+20%) 46,849 +2,250
Light→NH Low (-20%) 42,274 -2,325
Moderate→Intensive High (+20%) 44,992 +393
Moderate→Intensive Low (-20%) 44,152 -447
Moderate→NH High (+20%) 46,429 +1,830
Moderate→NH Low (-20%) 42,563 -2,036
# ── E8: Tornado plot ──────────────────────────────────────────────────────────
tornado_plot_data <- tornado_results |>
  select(parameter, direction, diff_from_base) |>
  pivot_wider(names_from = direction, values_from = diff_from_base) |>
  rename(low  = `Low (-20%)`,
         high = `High (+20%)`) |>
  mutate(
    range = abs(high) + abs(low),
    parameter = fct_reorder(parameter, range)
  )

tornado_plot_data |>
  ggplot() +
  geom_segment(
    aes(x = low, xend = high,
        y = parameter, yend = parameter),
    colour = "grey70", linewidth = 6
  ) +
  geom_segment(
    aes(x = pmin(low, 0), xend = pmax(high, 0),
        y = parameter, yend = parameter,
        colour = high > 0),
    linewidth = 6, alpha = 0.7
  ) +
  geom_vline(xintercept = 0, linewidth = 0.8, colour = "black") +
  scale_colour_manual(values = c("TRUE" = "#C00000", "FALSE" = "#4472C4"),
                      guide = "none") +
  scale_x_continuous(labels = comma) +
  labs(
    title    = "Tornado diagram — sensitivity of max NH residents",
    subtitle = paste("Each bar shows impact of ±20% change in one parameter.",
                     "Baseline max NH residents:", comma(round(base_max_nh))),
    x = "Change in maximum NH residents vs baseline",
    y = NULL
  ) +
  theme_minimal(base_size = 13) +
  theme(panel.grid.minor = element_blank(),
        panel.grid.major.y = element_blank())

# ── E9: Sensitivity of crossover year to intervention under each scenario ─────
# Does the 30% intervention still work under pessimistic assumptions?

cat("\nSensitivity of intervention effect across uncertainty scenarios:\n")
## 
## Sensitivity of intervention effect across uncertainty scenarios:
sensitivity_intervention_runs <- map_dfr(
  names(sensitivity_multipliers),
  function(sens_label) {
    sens_mult <- sensitivity_multipliers[sens_label]
    sens_base <- build_sensitivity_matrix(sens_mult)

    map_dfr(c(0, 0.20, 0.30, 0.40), function(int_strength) {

      # Apply intervention on top of sensitivity scenario
      m <- tryCatch(
        build_intervention_matrix(sens_base, int_strength),
        error = function(e) NULL
      )
      if (is.null(m)) return(tibble())

      result <- run_markov_open(
        transition_matrix = m,
        cohort_start      = cohort_2025,
        new_entrants_df   = new_entrants_proj,
        scenario_name     = "Strong ageing",
        start_year        = 2025
      )

      nh_traj <- result |>
        filter(state == "NH") |>
        mutate(over_capacity = n_people > national_supply_flat)

      crossover <- nh_traj |>
        filter(over_capacity) |>
        summarise(crossover = min(year)) |>
        pull(crossover)

      tibble(
        sensitivity    = sens_label,
        intervention   = paste0(int_strength * 100, "% reduction"),
        crossover_year = if_else(length(crossover) == 0,
                                 "No crossover", as.character(crossover)),
        max_nh         = round(max(nh_traj$n_people))
      )
    })
  }
)

sensitivity_intervention_runs |>
  select(sensitivity, intervention, crossover_year, max_nh) |>
  mutate(max_nh = comma(max_nh)) |>
  pivot_wider(names_from  = sensitivity,
              values_from = c(crossover_year, max_nh),
              names_glue  = "{sensitivity}_{.value}") |>
  select(intervention,
         starts_with("Low"),
         starts_with("Base"),
         starts_with("High")) |>
  kable(
    caption   = "Crossover year and max NH residents — intervention × sensitivity",
    col.names = c("Intervention",
                  "Low: crossover", "Low: max NH",
                  "Base: crossover", "Base: max NH",
                  "High: crossover", "High: max NH")
  ) |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 11)
Crossover year and max NH residents — intervention × sensitivity
Intervention Low: crossover Low: max NH Base: crossover Base: max NH High: crossover High: max NH
0% reduction Inf 36,635 2033 44,599 2030 52,096
20% reduction Inf 34,092 Inf 41,352 2032 48,228
30% reduction Inf 32,810 Inf 39,690 2032 46,217
40% reduction Inf 31,520 Inf 38,005 2033 44,155
# ════════════════════════════════════════════════════════════════════════════
# PART F — EXPORT DATA FOR INTERACTIVE DASHBOARD
# ════════════════════════════════════════════════════════════════════════════

library(jsonlite)

# Create data folder if it does not exist
dir.create("data", showWarnings = FALSE)

# ── F1: Model parameters ──────────────────────────────────────────────────────
model_params <- list(
  national_supply_flat  = national_supply_flat,
  supply_growth_rate    = SUPPLY_GROWTH_RATE,
  expansion_annual      = EXPANSION_ANNUAL,
  hc_entry_rate         = hc_entry_rate,
  avg_hc_duration       = avg_hc_duration_years,
  avg_nh_stay           = avg_nh_stay_years,
  unit_costs            = as.list(unit_costs),
  calibrated_probs = list(
    light_to_moderate    = round(calibrated_matrix["Light",    "Moderate"],  4),
    moderate_to_intensive= round(calibrated_matrix["Moderate", "Intensive"], 4),
    intensive_to_nh      = round(calibrated_matrix["Intensive","NH"],        4),
    light_to_nh          = round(calibrated_matrix["Light",    "NH"],        4),
    moderate_to_nh       = round(calibrated_matrix["Moderate", "NH"],        4),
    die_light            = p_die_light_obs,
    die_moderate         = p_die_moderate_obs,
    die_intensive        = p_die_intensive_obs,
    die_nh               = p_die_nh_obs
  ),
  calibration_targets = list(
    observed_light     = observed_light,
    observed_moderate  = observed_moderate,
    observed_intensive = observed_intensive,
    observed_nh        = observed_nh,
    observed_nh_admissions = observed_nh_admissions_annual
  ),
  cohort_start = as.list(cohort_2025)
)

write_json(model_params, "data/model_params.json", pretty = TRUE, auto_unbox = TRUE)
cat("Exported model_params.json\n")
## Exported model_params.json
# ── F2: NH trajectory — all intervention scenarios ────────────────────────────
nh_trajectories <- nh_scenarios |>
  select(intervention, year, nh_residents = n_people,
         supply, gap, over_capacity) |>
  mutate(
    nh_residents  = round(nh_residents),
    gap           = round(gap)
  )

write_json(nh_trajectories, "data/nh_trajectories.json",
           pretty = TRUE, auto_unbox = TRUE)
cat("Exported nh_trajectories.json\n")
## Exported nh_trajectories.json
# ── F3: Open cohort full state populations (baseline + 30% intervention) ──────
state_populations <- open_combined |>
  filter(scenario == "Strong ageing") |>
  bind_rows(
    scenarios_all |>
      filter(intervention == "30% reduction") |>
      mutate(scenario = "Strong ageing — 30% intervention")
  ) |>
  mutate(
    n_people = round(n_people),
    source   = if_else(is.na(intervention) | intervention == "0% reduction",
                       "Baseline", "30% intervention")
  ) |>
  select(source, scenario, year, state, n_people)

write_json(state_populations, "data/state_populations.json",
           pretty = TRUE, auto_unbox = TRUE)
cat("Exported state_populations.json\n")
## Exported state_populations.json
# ── F4: Crossover years ───────────────────────────────────────────────────────
crossover_export <- bind_rows(
  # By intervention level
  nh_scenarios |>
    filter(over_capacity) |>
    group_by(intervention) |>
    summarise(crossover_year = min(year), type = "intervention", .groups = "drop"),
  # By sensitivity scenario (baseline intervention only)
  nh_sensitivity |>
    filter(over_capacity) |>
    group_by(sensitivity) |>
    summarise(crossover_year = min(year), .groups = "drop") |>
    rename(intervention = sensitivity) |>
    mutate(type = "sensitivity")
)

write_json(crossover_export, "data/crossover_years.json",
           pretty = TRUE, auto_unbox = TRUE)
cat("Exported crossover_years.json\n")
## Exported crossover_years.json
# ── F5: Cost savings ──────────────────────────────────────────────────────────
cost_savings_export <- cost_savings |>
  select(intervention, year,
         total_cost_NOK, baseline_cost,
         annual_saving_NOK, annual_saving_MNOK) |>
  mutate(across(where(is.numeric), round))

write_json(cost_savings_export, "data/cost_savings.json",
           pretty = TRUE, auto_unbox = TRUE)
cat("Exported cost_savings.json\n")
## Exported cost_savings.json
# ── F6: Gap closure (fine-grained) ───────────────────────────────────────────
gap_closure_export <- crossover_fine |>
  filter(!is.na(max_nh)) |>
  mutate(
    gap            = max_nh - national_supply_flat,
    baseline_gap   = max_nh[strength == 0] - national_supply_flat,
    gap_closed_pct = round((1 - gap / baseline_gap) * 100, 1),
    gap            = round(gap)
  ) |>
  select(strength, max_nh, gap, gap_closed_pct, crossover)

write_json(gap_closure_export, "data/gap_closure.json",
           pretty = TRUE, auto_unbox = TRUE)
cat("Exported gap_closure.json\n")
## Exported gap_closure.json
# ── F7: Sensitivity results ───────────────────────────────────────────────────
sensitivity_export <- nh_sensitivity |>
  select(sensitivity, year, nh_residents = n_people,
         supply, gap, over_capacity) |>
  mutate(across(where(is.numeric), round))

write_json(sensitivity_export, "data/sensitivity.json",
           pretty = TRUE, auto_unbox = TRUE)
cat("Exported sensitivity.json\n")
## Exported sensitivity.json
# ── F8: National population projections ──────────────────────────────────────
pop_export <- national_proj |>
  mutate(population = round(population))

write_json(pop_export, "data/population_projections.json",
           pretty = TRUE, auto_unbox = TRUE)
cat("Exported population_projections.json\n")
## Exported population_projections.json
# ── F9: Municipality crossover ────────────────────────────────────────────────
# Check what columns exist after the join
muni_joined <- muni_action_needed |>
  left_join(muni_lookup, by = "muni_code")

# Use whatever name column exists
name_col <- intersect(names(muni_joined), c("muni_name", "muni_name.y", "muni_name.x"))

muni_export <- muni_joined |>
  mutate(across(where(is.numeric), ~ round(.x, 2))) |>
  rename(muni_name = any_of(name_col)[1]) |>
  select(muni_code, muni_name, year,
         total_elderly, projected_demand, supply,
         admissions_to_prevent, pct_reduction_needed,
         use_rate_reduction_pp)

write_json(muni_export, "data/municipality_crossover.json",
           pretty = TRUE, auto_unbox = TRUE)
cat("Exported municipality_crossover.json\n")
## Exported municipality_crossover.json
# ── F10: Summary confirmation ─────────────────────────────────────────────────
exported_files <- list.files("data", pattern = "\\.json$")
cat("\n=== EXPORT COMPLETE ===\n")
## 
## === EXPORT COMPLETE ===
cat("Files written to data/ folder:\n")
## Files written to data/ folder:
for (f in exported_files) {
  size <- file.size(file.path("data", f))
  cat(sprintf("  %-40s %s KB\n", f, round(size / 1024, 1)))
}
##   cost_savings.json                        8.1 KB
##   crossover_years.json                     0.4 KB
##   gap_closure.json                         12.7 KB
##   model_params.json                        0.9 KB
##   municipality_crossover.json              10.5 KB
##   nh_trajectories.json                     8.1 KB
##   population_projections.json              6.7 KB
##   sensitivity.json                         4.7 KB
##   state_populations.json                   14.9 KB
cat("\nNext step: run 'quarto render dashboard.qmd' in the terminal.\n")
## 
## Next step: run 'quarto render dashboard.qmd' in the terminal.