library(haven)
library(tidyverse)
library(knitr)
library(kableExtra)
library(scales) # Needed for clean currency/dollar formatting

file_path <- "C:\\Users\\JH\\Kitces.com\\DV - Research\\Anonymized Data and Codebooks - All Projects\\Productivity Studies\\2024\\fp24.dta"
df <- read_dta(file_path)

# define advice only
advice_only <- df %>% filter(grpadvice == 1)

# FIXED: Switched the median calculation split to rvxadv (Revenue Per Advisor)
med_rev <- median(advice_only$rvxadv, na.rm = TRUE)
med_rev_formatted <- dollar(med_rev, accuracy = 1) # Store formatted string for text below

processed_data <- advice_only %>% 
  # FIXED: Protect calculations from observations missing rvxadv data
  filter(!is.na(rvxadv)) %>% 
  mutate(
    # FIXED: Split performance segments using Revenue Per Advisor
    adv_seg = if_else(rvxadv <= med_rev, "Below Median Revenue Per Advisor", "Above Median Revenue Per Advisor"),
    total_weekly_hrs = rowSums(across(c(
      hrsprospect, hrsmarket, hrsprep, hrscurrentcl, hrsfinplan, 
      hrsinvestres, hrsinvestmgt, hrsclserv, hrsadmin, hrsmgmt, 
      hrscompliance, hrsproffdev, hrsoth
    )), na.rm = TRUE),
    total_weekly_hrs = if_else(is.na(hrscurrentcl) & is.na(hrsfinplan), NA_real_, total_weekly_hrs),
    pct_current_cl = (hrscurrentcl / total_weekly_hrs) * 100,
    pct_current_cl = if_else(is.infinite(pct_current_cl) | is.nan(pct_current_cl), NA_real_, pct_current_cl),
    pct_fin_plan = (hrsfinplan / total_weekly_hrs) * 100,
    pct_fin_plan = if_else(is.infinite(pct_fin_plan) | is.nan(pct_fin_plan), NA_real_, pct_fin_plan)
  ) %>% 
  mutate(
    missing_components = rowSums(across(c(
      pcvrretire, pcvrinvest, pcvrtxplan, pcvrlifeins, pcvrsave, pcvrcash, 
      pcvrestate, pcvrcollege, pcvr401k, pcvrltc, pcvrdisability, pcvroptions, 
      pcvrbenefits, pcvrcredit, pcvrproperty, pcvrhealth, pcvrstudent, pcvrbiz, 
      pcvrtxprep, pcvrcareer, pcvrestatedoc, pcvrheld401k, pcvrcharity, pcvrss, 
      pcvrmedicare, pcvrlifepl, pcvrrealestate, pcvroth
    ), is.na)),
    total_components = rowSums(across(c(
      pcvrretire, pcvrinvest, pcvrtxplan, pcvrlifeins, pcvrsave, pcvrcash, 
      pcvrestate, pcvrcollege, pcvr401k, pcvrltc, pcvrdisability, pcvroptions, 
      pcvrbenefits, pcvrcredit, pcvrproperty, pcvrhealth, pcvrstudent, pcvrbiz, 
      pcvrtxprep, pcvrcareer, pcvrestatedoc, pcvrheld401k, pcvrcharity, pcvrss, 
      pcvrmedicare, pcvrlifepl, pcvrrealestate, pcvroth
    )), na.rm = TRUE),
    total_components = if_else(missing_components == 28, NA_real_, total_components)
  )

# SUMMARY STATISTICS
summary_metrics <- processed_data %>% 
  group_by(adv_seg) %>% 
  summarise(
    meet_pct     = median(pct_current_cl, na.rm = TRUE),
    plan_pct     = median(pct_fin_plan, na.rm = TRUE),
    comp_med     = median(total_components, na.rm = TRUE),
    team_tot     = mean(ftetot, na.rm = TRUE),
    rev_client   = median(rvxclient, na.rm = TRUE),
    role_senior  = mean(haslead == 1, na.rm = TRUE) * 100,
    role_service = mean(hassrv == 1, na.rm = TRUE) * 100,
    role_assoc   = mean(hasass == 1, na.rm = TRUE) * 100,
    role_para    = mean(haspara == 1, na.rm = TRUE) * 100,
    role_admin   = mean(hascsa == 1, na.rm = TRUE) * 100
  )

# MATRIX RESHAPING & LABELS
final_table_df <- summary_metrics %>% 
  pivot_longer(
    cols = -adv_seg, 
    names_to = "Metric", 
    values_to = "Value"
  ) %>% 
  pivot_wider(
    names_from = adv_seg, 
    values_from = Value
  ) %>% 
  mutate(
    Row_Order = case_when(
      Metric == "meet_pct"     ~ 1,
      Metric == "plan_pct"     ~ 2,
      Metric == "comp_med"     ~ 3,
      Metric == "team_tot"     ~ 4,
      Metric == "rev_client"   ~ 5, 
      Metric == "role_senior"  ~ 7, 
      Metric == "role_service" ~ 8,
      Metric == "role_assoc"   ~ 9,
      Metric == "role_para"    ~ 10,
      Metric == "role_admin"   ~ 11
    )
  ) %>% 
  mutate(
    `Below Median Revenue Per Advisor` = case_when(
      Metric %in% c("meet_pct", "plan_pct", "role_senior", "role_service", "role_assoc", "role_para", "role_admin") 
        ~ sprintf("%.0f%%", round(`Below Median Revenue Per Advisor`)),
      Metric %in% c("comp_med", "team_tot") 
        ~ sprintf("%.0f", round(`Below Median Revenue Per Advisor`)),
      Metric == "rev_client"
        ~ dollar(`Below Median Revenue Per Advisor`, accuracy = 1),
      TRUE ~ as.character(`Below Median Revenue Per Advisor`)
    ),
    `Above Median Revenue Per Advisor` = case_when(
      Metric %in% c("meet_pct", "plan_pct", "role_senior", "role_service", "role_assoc", "role_para", "role_admin") 
        ~ sprintf("%.0f%%", round(`Above Median Revenue Per Advisor`)),
      Metric %in% c("comp_med", "team_tot") 
        ~ sprintf("%.0f", round(`Above Median Revenue Per Advisor`)),
      Metric == "rev_client"
        ~ dollar(`Above Median Revenue Per Advisor`, accuracy = 1),
      TRUE ~ as.character(`Above Median Revenue Per Advisor`)
    ),
    Metric = case_when(
      Metric == "meet_pct"     ~ "% Of Weekly Hours Spent On Meeting With Current Clients",
      Metric == "plan_pct"     ~ "% Of Weekly Hours Spent On Financial Plan Preparation",
      Metric == "comp_med"     ~ "Median Components Included In Financial Plans",
      Metric == "team_tot"     ~ "Mean Total Service Team Members (FTE)",
      Metric == "rev_client"   ~ "Median Revenue Per Client",
      Metric == "role_senior"  ~ "Senior Advisors",
      Metric == "role_service" ~ "Service Advisors",
      Metric == "role_assoc"   ~ "Associate Advisors",
      Metric == "role_para"    ~ "Paraplanners",
      Metric == "role_admin"   ~ "Admin / CSA"
    )
  ) %>% 
  add_row(
    Metric = "% Of Teams With This Role Present", 
    `Below Median Revenue Per Advisor` = "", 
    `Above Median Revenue Per Advisor` = "", 
    Row_Order = 6 
  ) %>% 
  arrange(Row_Order) %>% 
  select(Metric, `Below Median Revenue Per Advisor`, `Above Median Revenue Per Advisor`)

# TABLE
kable(
  final_table_df, 
  align = c("l", "c", "c"), 
  col.names = c("Metric", "Below Median Revenue Per Advisor", "Above Median Revenue Per Advisor"),
  caption = "Advice-Only Practices By Median Revenue Per Advisor"
) %>% 
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"), 
    full_width = FALSE, 
    position = "left"
  ) %>% 
  row_spec(
    0, bold = TRUE, color = "white", background = "#1B365D"
  ) %>% 
  row_spec(
    6, bold = FALSE, color = "#000000", background = "#E8EEF5" 
  ) %>% 
  add_indent(c(7, 8, 9, 10, 11))
Advice-Only Practices By Median Revenue Per Advisor
Metric Below Median Revenue Per Advisor Above Median Revenue Per Advisor
% Of Weekly Hours Spent On Meeting With Current Clients 11% 20%
% Of Weekly Hours Spent On Financial Plan Preparation 20% 15%
Median Components Included In Financial Plans 14 18
Mean Total Service Team Members (FTE) 1 3
Median Revenue Per Client $2,353 $4,891
% Of Teams With This Role Present
Senior Advisors 100% 100%
Service Advisors 0% 29%
Associate Advisors 0% 14%
Paraplanners 0% 21%
Admin / CSA 7% 21%
# Dynamically prints the precise value of the median split dollar amount
cat(paste0("Note: The median Revenue Per Advisor is ", med_rev_formatted, ".\n\n"))
Note: The median Revenue Per Advisor is $172,500.
# Target interpretation text required by your teammate
cat("What makes advice-only advisors productive is the same thing that makes all advisors productive: having a team to leverage and support you, and using that added leverage to offer the more complex planning work that higher-value clients want/have the financial wherewithal to pay for.\n")
What makes advice-only advisors productive is the same thing that makes all advisors productive: having a team to leverage and support you, and using that added leverage to offer the more complex planning work that higher-value clients want/have the financial wherewithal to pay for.