Sectoral Scan: Social Sector Expenditure Across Indian States

An Examination of Education, Health, and Nutrition Expenditure (RBI & UDISE+)

Author

ESU

Published

August 12, 2026

1 Introduction & Methodology

This report analyses public expenditure trends across selected Indian states (Karnataka, Tamil Nadu, Maharashtra and Odisha) using macro-fiscal data from the Reserve Bank of India (RBI) State Finances, GSDP data from MoSPI and school education statistics from UDISE+.

1.1 Budget Accounting Definitions

  • Revenue Expenditure (RevEx): Government spending on recurring operational costs (salaries, maintenance, administrative expenses).
  • Capital Expenditure (CapEx): Investment in permanent physical asset creation (school buildings, labs, infrastructure).
  • Developmental Expenditure (DevEx): Expenditure directly targeted at social and economic service delivery.
  • Non-Developmental Expenditure (Non-DevEx): Spending on general administrative overhead and statutory functions.

2 Expenditure Analysis

2.1 Between States: Expenditure as a Percentage of GSDP

Show R Code
#| label: Between States: Expenditure as a Percentage of GSDP
#| fig-width: 10
#| fig-height: 6
#| fig-dpi: 300
#| out-width: "100%"

# ==============================================================================
# Corrected EHN Macro Analysis (Explicitly using Appendix 2 for RevEx, 4 for CapEx)
# ==============================================================================

library(tidyverse)
library(janitor)
library(readxl)

# ---------------------------------------------------------
# 1. Define Paths & Helpers
# ---------------------------------------------------------
rbi_path <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/rbiestates/ESTATES23012026.XLSX"
gsdp_path <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/longdf/Bernstein_Master_GVA_GSDP_Long.csv"

target_states <- c("Karnataka", "Tamil Nadu", "Maharashtra", "Odisha")

fix_states <- function(x) {
  case_when(
    str_detect(x, "(?i)Delhi") ~ "Delhi",
    str_detect(x, "(?i)Jammu") ~ "Jammu & Kashmir",
    str_detect(x, "(?i)Andaman") ~ "Andaman & Nicobar Islands",
    TRUE ~ str_trim(x)
  )
}

# ---------------------------------------------------------
# 2. Load and Clean Raw Data
# ---------------------------------------------------------
rbi_raw <- read_excel(rbi_path, sheet = "Data") %>% clean_names()
gsdp_raw <- read_csv(gsdp_path, show_col_types = FALSE) %>% clean_names()

# Standardize GSDP (Current Prices)
gsdp_curr <- gsdp_raw %>%
  mutate(state = fix_states(state), year = as.character(year)) %>%
  filter(metric == 'GSDP', price_base == "Current") %>%
  select(state, year, gsdp_cr = value)

# Standardize RBI (Extract best expenditure value)
rbi_clean <- rbi_raw %>%
  rename(state = state_ut, year = fiscal_year) %>%
  mutate(
    state = fix_states(state),
    year = str_replace(year, "^(\\d{4})-\\d{2}(\\d{2})$", "\\1-\\2"),
    # Pick Account, else Revised, else Budget
    exp_cr = case_when(account > 0 ~ account, revised > 0 ~ revised, budget > 0 ~ budget, TRUE ~ NA_real_)
  ) %>%
  filter(!is.na(exp_cr))

# 1. Map all specific budget heads to their precise economic buckets
rbi_metrics <- rbi_clean %>%
  mutate(
    var_name = case_when(
      # ==========================================
      # 1. STATE GRAND TOTALS
      # ==========================================
      # Absolute RevEx & CapEx
      appendix == "Appendix-2" & budget_head == "Total: TOTAL EXPENDITURE (I+II+III)" ~ "state_total_revex",
      appendix == "Appendix-4" & budget_head == "Total$: TOTAL CAPITAL DISBURSEMENTS (Excluding Public Accounts)" ~ "state_total_capex",
      
      # State Developmental vs Non-Developmental (Using str_detect to bypass varying suffix letters)
      appendix == "Appendix-2" & str_detect(budget_head, "^I: DEVELOPMENTAL EXPENDITURE") ~ "state_dev_revex",
      appendix == "Appendix-2" & str_detect(budget_head, "^II: NON-DEVELOPMENTAL EXPENDITURE") ~ "state_nondev_revex",
      
      appendix == "Appendix-4" & str_detect(budget_head, "^I\\.1: Development") ~ "state_dev_capex_outlay",
      appendix == "Appendix-4" & str_detect(budget_head, "^I\\.2: Non-Development") ~ "state_nondev_capex_outlay",
      appendix == "Appendix-4" & str_detect(budget_head, "^IV\\.1: Development Purposes") ~ "state_dev_capex_loan",
      appendix == "Appendix-4" & str_detect(budget_head, "^IV\\.2: Non-Development Purposes") ~ "state_nondev_capex_loan",
      # ==========================================
      # 2. EDUCATION SECTOR
      # ==========================================
      # RevEx
      appendix == "Appendix-2" & budget_head == "I.A.1: Education, Sports, Art and Culture" ~ "edu_dev_revex",
      appendix == "Appendix-2" & budget_head == "II.C.4.i: Education, Sports, Art and Culture" ~ "edu_nondev_revex", # Cess transfer removed
      
      # CapEx
      appendix == "Appendix-4" & budget_head == "I.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head == "IV.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_loan",

      # ==========================================
      # 3. HEALTH SECTOR
      # ==========================================
      # RevEx
      appendix == "Appendix-2" & budget_head %in% c("I.A.2: Medical and Public Health", 
                                                    "I.A.3: Family Welfare") ~ "health_dev_revex",
      appendix == "Appendix-2" & budget_head %in% c("II.C.4.ii: Medical and Public Health", 
                                                    "II.C.4.iii: Family Welfare") ~ "health_nondev_revex",
      # CapEx
      appendix == "Appendix-4" & budget_head %in% c("I.1.a.2: Medical and Public Health", 
                                                    "I.1.a.3: Family Welfare") ~ "health_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head %in% c("IV.1.a.2: Medical and Public Health", 
                                                    "IV.1.a.3: Family Welfare") ~ "health_dev_capex_loan",

      # ==========================================
      # 4. NUTRITION SECTOR
      # ==========================================
      # RevEx
      appendix == "Appendix-2" & budget_head == "I.A.10: Nutrition" ~ "nut_dev_revex",
      # (Nutrition has no Non-Dev or CapEx equivalents under standard budget heads)
      
      TRUE ~ NA_character_
    )
  ) %>%
  filter(!is.na(var_name)) %>%
  # Group by state, year, and specific variable, then sum to handle any combined heads
  group_by(state, year, var_name) %>%
  summarise(exp = sum(exp_cr, na.rm = TRUE), .groups = "drop") %>%
  # Pivot wider so every metric becomes its own column (fill missing years/metrics with 0)
  pivot_wider(names_from = var_name, values_from = exp, values_fill = 0)

# 2. Ensure all columns exist just in case a state/year had exactly 0 in a specific bucket
cols_needed <- c(
  "state_total_revex", "state_total_capex", "state_dev_revex", "state_nondev_revex", 
  "state_dev_capex_outlay", "state_nondev_capex_outlay", "state_dev_capex_loan", "state_nondev_capex_loan",
  "edu_dev_revex", "edu_nondev_revex", "edu_dev_capex_outlay", "edu_dev_capex_loan",
  "health_dev_revex", "health_nondev_revex", "health_dev_capex_outlay", "health_dev_capex_loan",
  "nut_dev_revex"
)
for(col in cols_needed) { if(!col %in% names(rbi_metrics)) rbi_metrics[[col]] <- 0 }

# 3. Calculate the Final Aggregated Totals
rbi_final_totals <- rbi_metrics %>%
  mutate(
    # --- State Grand Totals ---
    state_grand_total_exp = state_total_revex + state_total_capex,
    state_total_dev_exp   = state_dev_revex + state_dev_capex_outlay + state_dev_capex_loan,
    state_total_nondev_exp= state_nondev_revex + state_nondev_capex_outlay + state_nondev_capex_loan,
    
    # --- Sector Grand Totals ---
    # Total Education
    edu_total_revex = edu_dev_revex + edu_nondev_revex,
    edu_total_capex = edu_dev_capex_outlay + edu_dev_capex_loan,
    edu_grand_total = edu_total_revex + edu_total_capex,
    
    # Total Health
    health_total_revex = health_dev_revex + health_nondev_revex,
    health_total_capex = health_dev_capex_outlay + health_dev_capex_loan,
    health_grand_total = health_total_revex + health_total_capex,
    
    # Total Nutrition
    nut_total_revex = nut_dev_revex,
    nut_total_capex = 0, # Hardcoded to 0 based on structure
    nut_grand_total = nut_total_revex + nut_total_capex
  )

# ---------------------------------------------------------
# 4. Merge Data and Calculate Grand Totals & GSDP %
# ---------------------------------------------------------
master_df <- gsdp_curr %>%
  left_join(rbi_metrics, by = c("state", "year")) %>%
  mutate(
    # Calculate Overall State Totals
    total_exp = state_total_revex + state_total_capex,
    
    # Calculate Sector RevEx Totals (Dev + Non-Dev)
    edu_revex    = edu_dev_revex + edu_nondev_revex,
    health_revex = health_dev_revex + health_nondev_revex,
    nut_revex    = nut_dev_revex,
    
    # Calculate Sector CapEx Totals (Outlays + Loans)
    edu_capex    = edu_dev_capex_outlay + edu_dev_capex_loan,
    health_capex = health_dev_capex_outlay + health_dev_capex_loan,
    nut_capex    = 0, # Hardcoded based on RBI structure
    
    # Calculate EHN Sector Grand Totals (RevEx + CapEx)
    tot_edu_exp    = edu_revex + edu_capex,
    tot_health_exp = health_revex + health_capex,
    tot_nut_exp    = nut_revex + nut_capex,
    
    # Calculate as % of GSDP
    pct_gsdp_tot_exp    = if_else(gsdp_cr > 0, (total_exp / gsdp_cr) * 100, NA_real_),
    pct_gsdp_tot_edu    = if_else(gsdp_cr > 0, (tot_edu_exp / gsdp_cr) * 100, NA_real_),
    pct_gsdp_tot_health = if_else(gsdp_cr > 0, (tot_health_exp / gsdp_cr) * 100, NA_real_),
    pct_gsdp_tot_nut    = if_else(gsdp_cr > 0, (tot_nut_exp / gsdp_cr) * 100, NA_real_)
  ) %>%
  # Organize column order perfectly as requested, including the granular splits
  select(
    state, year, gsdp_cr, 
    # State Overalls
    state_total_revex, state_total_capex, total_exp, 
    # Aggregated Sector Metrics
    edu_revex, edu_capex, tot_edu_exp,
    health_revex, health_capex, tot_health_exp,
    nut_revex, nut_capex, tot_nut_exp,
    # GSDP Percentages
    pct_gsdp_tot_exp, pct_gsdp_tot_edu, pct_gsdp_tot_health, pct_gsdp_tot_nut,
    # Granular Splits (Kept at the end for auditing/transparency)
    edu_dev_revex, edu_nondev_revex, edu_dev_capex_outlay, edu_dev_capex_loan,
    health_dev_revex, health_nondev_revex, health_dev_capex_outlay, health_dev_capex_loan,
    nut_dev_revex
  ) %>%
  arrange(state, year)

# ---------------------------------------------------------
# 5. Filter for Target States & Export CSVs
# ---------------------------------------------------------
selected_states_df <- master_df %>%
  filter(state %in% target_states)

# Saving results commented out for Quarto rendering
# write_csv(master_df, "All_States_Detailed_EHN_Capex_Revex.csv")
# write_csv(selected_states_df, "Selected_States_Detailed_EHN_Capex_Revex.csv")

cat("\n[SUCCESS] Data processing complete.\n")

[SUCCESS] Data processing complete.
Show R Code
# =========================================================
# PLOTTING SECTION
# =========================================================

# Prepare plot data with numeric year for continuous X-axis scaling
plot_data <- selected_states_df %>%
  mutate(year_num = as.numeric(str_extract(year, "^\\d{4}")))

# Custom publication theme optimized to prevent caption cropping
# Custom publication theme updated to PREVENT CROPPING

# ---------------------------------------------------------
# MODIFIED THEME: Scaled down text to fit long captions/titles
# ---------------------------------------------------------
theme_publication <- function() {
  theme_bw() +
    theme(
      plot.title       = element_text(hjust = 0.5, face = "bold", size = 14), # Reduced from 18 to 14
      panel.border     = element_rect(colour = "black", fill = NA, linewidth = 1.2), # Thinned border slightly
      axis.title       = element_text(face = "bold", size = 12), # Reduced from 14 to 12
      axis.text        = element_text(face = "bold", size = 10, color = "black"), # Reduced from 12 to 10
      axis.text.x      = element_text(angle = 45, hjust = 1),
      plot.caption     = element_text(face = "bold", hjust = 0, size = 8.5, lineheight = 1.1), # Reduced from 10 to 8.5 to fit long URLs
      strip.background = element_rect(fill = "grey90", color = "black", linewidth = 1),
      strip.text       = element_text(face = "bold", size = 11),
      legend.position  = "top",
      legend.title     = element_text(face = "bold", size = 11),
      legend.text      = element_text(size = 11),
      
      # CRITICAL FIX: Align title and caption to the full plot width, not just the panel
      plot.title.position = "plot",
      plot.caption.position = "plot",
      
      # Unified padding
      plot.margin      = margin(t = 10, r = 10, b = 15, l = 10)
    )
}

# ---------------------------------------------------------
# 1. Total Expenditure Plot
# ---------------------------------------------------------
a1 <- "Source: TOTAL EXPENDITURE (I+II+III) - RBI State Finances (eSTATES Database), GSDP - MoSPI."

p_tot <- ggplot(plot_data, aes(x = year_num, y = pct_gsdp_tot_exp, color = state, group = state)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_x_continuous(breaks = unique(plot_data$year_num), labels = unique(plot_data$year)) +
  scale_y_continuous(labels = function(x) paste0(round(x, 1), "%")) +
  labs(
    title = "Total State Expenditure as a Percentage of Current GSDP",
    x = "Financial Year",
    y = "Percentage of GSDP",
    color = "State",
    caption = a1
  ) +
  theme_publication()

print(p_tot)

Show R Code
# ---------------------------------------------------------
# 2. Education Expenditure Plot
# ---------------------------------------------------------
edu_only_rbi_gsdp_caption = "Source: Education Sector Expenditure - Education, Sports, Art and Culture [I.A.1 + II.C.4.i + I.1.a.1 + IV.1.a.1]\nRBI State Finances (eSTATES Database), GSDP - MoSPI."

p_edu <- ggplot(plot_data, aes(x = year_num, y = pct_gsdp_tot_edu, color = state, group = state)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_x_continuous(breaks = unique(plot_data$year_num), labels = unique(plot_data$year)) +
  scale_y_continuous(labels = function(x) paste0(round(x, 1), "%")) +
  labs(
    title = "Education Sector Expenditure as a Percentage of Current GSDP",
    x = "Financial Year",
    y = "Percentage of GSDP",
    color = "State",
    caption = edu_only_rbi_gsdp_caption
  ) +
  theme_publication()

print(p_edu)

Show R Code
# ---------------------------------------------------------
# 3. Health Expenditure Plot
# ---------------------------------------------------------
health_only_rbi_gsdp_caption = "Source: RBI State Finances (eSTATES Database), GSDP - MoSPI.\nHealth Sector - Medical and Public Health [I.A.2 II.C.4.ii I.1.a.2 IV.1.a.2] & Family Welfare [I.A.3 II.C.4.iii I.1.a.3 IV.1.a.3]."


p_health <- ggplot(plot_data, aes(x = year_num, y = pct_gsdp_tot_health, color = state, group = state)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_x_continuous(breaks = unique(plot_data$year_num), labels = unique(plot_data$year)) +
  scale_y_continuous(labels = function(x) paste0(round(x, 1), "%")) +
  labs(
    title = "Health Sector Expenditure as a Percentage of Current GSDP",
    x = "Financial Year",
    y = "Percentage of GSDP",
    color = "State",
    caption = health_only_rbi_gsdp_caption
  ) +
  theme_publication()

print(p_health)

Show R Code
# ---------------------------------------------------------
# 4. Nutrition Expenditure Plot
# ---------------------------------------------------------
nut_only_rbi_gsdp_caption = "Source: Nutrition Sector Expenditure [I.A.10] - RBI State Finances (eSTATES Database), GSDP - MoSPI."

p_nut <- ggplot(plot_data, aes(x = year_num, y = pct_gsdp_tot_nut, color = state, group = state)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_x_continuous(breaks = unique(plot_data$year_num), labels = unique(plot_data$year)) +
  scale_y_continuous(labels = function(x) paste0(round(x, 2), "%")) +
  labs(
    title = "Nutrition Sector Expenditure as a Percentage of Current GSDP",
    x = "Financial Year",
    y = "Percentage of GSDP",
    color = "State",
    caption = nut_only_rbi_gsdp_caption
  ) +
  theme_publication()

print(p_nut)

2.2 Within States: Expenditure as a Percentage of GSDP

Show R Code
#| label: Within States: Expenditure as a Percentage of GSDP
#| fig-width: 12
#| fig-height: 7
#| out-width: "100%"
#| message: false
#| warning: false

library(ggplot2)
library(dplyr)
library(tidyr)
library(stringr)

# ---------------------------------------------------------
# 1. Setup Theme & Caption
# ---------------------------------------------------------
b <- "Source: RBI State Finances (eSTATES Database), GSDP - MoSPI.\nEducation: Education, Sports, Art and Culture [I.A.1 + II.C.4.i + I.1.a.1 + IV.1.a.1]. Nutrition [I.A.10].\nHealth: Medical and Public Health [I.A.2 + II.C.4.ii + I.1.a.2 + IV.1.a.2] & Family Welfare [I.A.3 + II.C.4.iii + I.1.a.3 + IV.1.a.3]."

# Reusing the modified theme that prevents clipping
theme_publication <- function() {
  theme_bw() +
    theme(
      plot.title       = element_text(hjust = 0.5, face = "bold", size = 14), 
      panel.border     = element_rect(colour = "black", fill = NA, linewidth = 1.2), 
      axis.title       = element_text(face = "bold", size = 12), 
      axis.text        = element_text(face = "bold", size = 10, color = "black"), 
      axis.text.x      = element_text(angle = 45, hjust = 1),
      plot.caption     = element_text(face = "bold", hjust = 0, size = 8.5, lineheight = 1.1), 
      strip.background = element_rect(fill = "grey90", color = "black", linewidth = 1),
      strip.text       = element_text(face = "bold", size = 11),
      legend.position  = "top",
      legend.title     = element_text(face = "bold", size = 11),
      legend.text      = element_text(size = 11),
      
      # CRITICAL FIX: Align title and caption to the full plot width
      plot.title.position = "plot",
      plot.caption.position = "plot",
      
      # Unified padding
      plot.margin      = margin(t = 10, r = 10, b = 15, l = 10)
    )
}

# ---------------------------------------------------------
# 2. Reshape Data to Long Format for Multi-Line Legend (EHN ONLY)
# ---------------------------------------------------------
# Using master_df (calculated in previous chunks) to plot
state_plot_data <- master_df %>%
  mutate(year_num = as.numeric(str_extract(year, "^\\d{4}"))) %>%
  # Pivot only the three EHN percentage metrics into a single column
  pivot_longer(
    cols = c(pct_gsdp_tot_edu, pct_gsdp_tot_health, pct_gsdp_tot_nut),
    names_to = "metric",
    values_to = "pct_gsdp"
  ) %>%
  mutate(
    # Clean names for the chart legend
    metric = case_when(
      metric == "pct_gsdp_tot_edu"    ~ "Education Expenditure",
      metric == "pct_gsdp_tot_health" ~ "Health Expenditure",
      metric == "pct_gsdp_tot_nut"    ~ "Nutrition Expenditure"
    ),
    # Lock legend factor order (Total Expenditure removed)
    metric = factor(metric, levels = c("Education Expenditure", "Health Expenditure", "Nutrition Expenditure"))
  )

# ---------------------------------------------------------
# 3. Loop & Generate Individual Plots for Configured States
# ---------------------------------------------------------
# Pulls target_states from the earlier chunk (e.g., Karnataka, Tamil Nadu, etc.)
all_states <- target_states

for (st in all_states) {
  
  # Filter data for current state
  df_state <- state_plot_data %>% filter(state == st)
  
  # Skip plotting if the state isn't in the dataset
  if(nrow(df_state) == 0) next
  
  # Build plot
  p <- ggplot(df_state, aes(x = year_num, y = pct_gsdp, color = metric, group = metric)) +
    geom_line(linewidth = 1.2) +
    geom_point(size = 3) +
    scale_x_continuous(breaks = unique(df_state$year_num), labels = unique(df_state$year)) +
    scale_y_continuous(labels = function(x) paste0(round(x, 1), "%")) +
    scale_color_manual(
      values = c(
        "Education Expenditure" = "#2ca02c", # Green
        "Health Expenditure"    = "#ff7f0e", # Orange
        "Nutrition Expenditure" = "#d62728"  # Red
      )
    ) +
    labs(
      title   = str_wrap(paste0(st, " Sectoral Expenditure as a Percentage of Current GSDP"), width = 90),
      x       = "Financial Year",
      y       = "Percentage of GSDP",
      color   = "Sector",
      caption = b # Prevents long text from cutting off
    ) +
    theme_publication()
  
  # Print plot to Quarto HTML document
  print(p)
}

2.3 Within States: Sectoral Expenditure as a Percentage of total State Expenditure

Show R Code
#| label: between-states-expenditure-as-percentage-of-total-expenditure
#| fig-width: 10
#| fig-height: 6
#| fig-dpi: 300
#| out-width: "100%"

# ==============================================================================
# Corrected EHN Macro Analysis (Explicitly using Appendix 2 for RevEx, 4 for CapEx)
# ==============================================================================

library(tidyverse)
library(janitor)
library(readxl)
library(stringr)

# ---------------------------------------------------------
# 1. Define Paths & Helpers
# ---------------------------------------------------------
rbi_path <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/rbiestates/ESTATES23012026.XLSX"
gsdp_path <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/longdf/Bernstein_Master_GVA_GSDP_Long.csv"

target_states <- c("Karnataka", "Tamil Nadu", "Maharashtra", "Odisha")

fix_states <- function(x) {
  case_when(
    str_detect(x, "(?i)Delhi") ~ "Delhi",
    str_detect(x, "(?i)Jammu") ~ "Jammu & Kashmir",
    str_detect(x, "(?i)Andaman") ~ "Andaman & Nicobar Islands",
    TRUE ~ str_trim(x)
  )
}

# ---------------------------------------------------------
# 2. Load and Clean Raw Data
# ---------------------------------------------------------
rbi_raw <- read_excel(rbi_path, sheet = "Data") %>% clean_names()
gsdp_raw <- read_csv(gsdp_path, show_col_types = FALSE) %>% clean_names()

# Standardize GSDP (Current Prices)
gsdp_curr <- gsdp_raw %>%
  mutate(state = fix_states(state), year = as.character(year)) %>%
  filter(metric == 'GSDP', price_base == "Current") %>%
  select(state, year, gsdp_cr = value)

# Standardize RBI (Extract best expenditure value)
rbi_clean <- rbi_raw %>%
  rename(state = state_ut, year = fiscal_year) %>%
  mutate(
    state = fix_states(state),
    year = str_replace(year, "^(\\d{4})-\\d{2}(\\d{2})$", "\\1-\\2"),
    # Pick Account, else Revised, else Budget
    exp_cr = case_when(account > 0 ~ account, revised > 0 ~ revised, budget > 0 ~ budget, TRUE ~ NA_real_)
  ) %>%
  filter(!is.na(exp_cr))

# 1. Map all specific budget heads to their precise economic buckets
rbi_metrics <- rbi_clean %>%
  mutate(
    var_name = case_when(
      # ==========================================
      # 1. STATE GRAND TOTALS
      # ==========================================
      # Absolute RevEx & CapEx
      appendix == "Appendix-2" & budget_head == "Total: TOTAL EXPENDITURE (I+II+III)" ~ "state_total_revex",
      appendix == "Appendix-4" & budget_head == "Total$: TOTAL CAPITAL DISBURSEMENTS (Excluding Public Accounts)" ~ "state_total_capex",
      
      # State Developmental vs Non-Developmental (Using str_detect to bypass varying suffix letters)
      appendix == "Appendix-2" & str_detect(budget_head, "^I: DEVELOPMENTAL EXPENDITURE") ~ "state_dev_revex",
      appendix == "Appendix-2" & str_detect(budget_head, "^II: NON-DEVELOPMENTAL EXPENDITURE") ~ "state_nondev_revex",
      
      appendix == "Appendix-4" & str_detect(budget_head, "^I\\.1: Development") ~ "state_dev_capex_outlay",
      appendix == "Appendix-4" & str_detect(budget_head, "^I\\.2: Non-Development") ~ "state_nondev_capex_outlay",
      appendix == "Appendix-4" & str_detect(budget_head, "^IV\\.1: Development Purposes") ~ "state_dev_capex_loan",
      appendix == "Appendix-4" & str_detect(budget_head, "^IV\\.2: Non-Development Purposes") ~ "state_nondev_capex_loan",
      # ==========================================
      # 2. EDUCATION SECTOR
      # ==========================================
      # RevEx
      appendix == "Appendix-2" & budget_head == "I.A.1: Education, Sports, Art and Culture" ~ "edu_dev_revex",
      appendix == "Appendix-2" & budget_head == "II.C.4.i: Education, Sports, Art and Culture" ~ "edu_nondev_revex", # Cess transfer removed
      
      # CapEx
      appendix == "Appendix-4" & budget_head == "I.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head == "IV.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_loan",

      # ==========================================
      # 3. HEALTH SECTOR
      # ==========================================
      # RevEx
      appendix == "Appendix-2" & budget_head %in% c("I.A.2: Medical and Public Health", 
                                                    "I.A.3: Family Welfare") ~ "health_dev_revex",
      appendix == "Appendix-2" & budget_head %in% c("II.C.4.ii: Medical and Public Health", 
                                                    "II.C.4.iii: Family Welfare") ~ "health_nondev_revex",
      # CapEx
      appendix == "Appendix-4" & budget_head %in% c("I.1.a.2: Medical and Public Health", 
                                                    "I.1.a.3: Family Welfare") ~ "health_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head %in% c("IV.1.a.2: Medical and Public Health", 
                                                    "IV.1.a.3: Family Welfare") ~ "health_dev_capex_loan",

      # ==========================================
      # 4. NUTRITION SECTOR
      # ==========================================
      # RevEx
      appendix == "Appendix-2" & budget_head == "I.A.10: Nutrition" ~ "nut_dev_revex",
      # (Nutrition has no Non-Dev or CapEx equivalents under standard budget heads)
      
      TRUE ~ NA_character_
    )
  ) %>%
  filter(!is.na(var_name)) %>%
  # Group by state, year, and specific variable, then sum to handle any combined heads
  group_by(state, year, var_name) %>%
  summarise(exp = sum(exp_cr, na.rm = TRUE), .groups = "drop") %>%
  # Pivot wider so every metric becomes its own column (fill missing years/metrics with 0)
  pivot_wider(names_from = var_name, values_from = exp, values_fill = 0)

# 2. Ensure all columns exist just in case a state/year had exactly 0 in a specific bucket
cols_needed <- c(
  "state_total_revex", "state_total_capex", "state_dev_revex", "state_nondev_revex", 
  "state_dev_capex_outlay", "state_nondev_capex_outlay", "state_dev_capex_loan", "state_nondev_capex_loan",
  "edu_dev_revex", "edu_nondev_revex", "edu_dev_capex_outlay", "edu_dev_capex_loan",
  "health_dev_revex", "health_nondev_revex", "health_dev_capex_outlay", "health_dev_capex_loan",
  "nut_dev_revex"
)
for(col in cols_needed) { if(!col %in% names(rbi_metrics)) rbi_metrics[[col]] <- 0 }

# 3. Calculate the Final Aggregated Totals
rbi_final_totals <- rbi_metrics %>%
  mutate(
    # --- State Grand Totals ---
    state_grand_total_exp = state_total_revex + state_total_capex,
    state_total_dev_exp   = state_dev_revex + state_dev_capex_outlay + state_dev_capex_loan,
    state_total_nondev_exp= state_nondev_revex + state_nondev_capex_outlay + state_nondev_capex_loan,
    
    # --- Sector Grand Totals ---
    # Total Education
    edu_total_revex = edu_dev_revex + edu_nondev_revex,
    edu_total_capex = edu_dev_capex_outlay + edu_dev_capex_loan,
    edu_grand_total = edu_total_revex + edu_total_capex,
    
    # Total Health
    health_total_revex = health_dev_revex + health_nondev_revex,
    health_total_capex = health_dev_capex_outlay + health_dev_capex_loan,
    health_grand_total = health_total_revex + health_total_capex,
    
    # Total Nutrition
    nut_total_revex = nut_dev_revex,
    nut_total_capex = 0, # Hardcoded to 0 based on structure
    nut_grand_total = nut_total_revex + nut_total_capex
  )

# ---------------------------------------------------------
# 4. Merge Data and Calculate Grand Totals & GSDP %
# ---------------------------------------------------------
master_df <- gsdp_curr %>%
  left_join(rbi_metrics, by = c("state", "year")) %>%
  mutate(
    # Calculate Overall State Totals
    total_exp = state_total_revex + state_total_capex,
    
    # Calculate Sector RevEx Totals (Dev + Non-Dev)
    edu_revex    = edu_dev_revex + edu_nondev_revex,
    health_revex = health_dev_revex + health_nondev_revex,
    nut_revex    = nut_dev_revex,
    
    # Calculate Sector CapEx Totals (Outlays + Loans)
    edu_capex    = edu_dev_capex_outlay + edu_dev_capex_loan,
    health_capex = health_dev_capex_outlay + health_dev_capex_loan,
    nut_capex    = 0, # Hardcoded based on RBI structure
    
    # Calculate EHN Sector Grand Totals (RevEx + CapEx)
    tot_edu_exp    = edu_revex + edu_capex,
    tot_health_exp = health_revex + health_capex,
    tot_nut_exp    = nut_revex + nut_capex,
    
    # Calculate as % of TOTAL EXPENDITURE (Modified Step)
    pct_totexp_tot_exp    = if_else(total_exp > 0, (total_exp / total_exp) * 100, NA_real_), # Will be 100%
    pct_totexp_tot_edu    = if_else(total_exp > 0, (tot_edu_exp / total_exp) * 100, NA_real_),
    pct_totexp_tot_health = if_else(total_exp > 0, (tot_health_exp / total_exp) * 100, NA_real_),
    pct_totexp_tot_nut    = if_else(total_exp > 0, (tot_nut_exp / total_exp) * 100, NA_real_)
  ) %>%
  # Organize column order perfectly as requested, including the granular splits
  select(
    state, year, gsdp_cr, 
    # State Overalls
    state_total_revex, state_total_capex, total_exp, 
    # Aggregated Sector Metrics
    edu_revex, edu_capex, tot_edu_exp,
    health_revex, health_capex, tot_health_exp,
    nut_revex, nut_capex, tot_nut_exp,
    # Total Expenditure Percentages
    pct_totexp_tot_exp, pct_totexp_tot_edu, pct_totexp_tot_health, pct_totexp_tot_nut,
    # Granular Splits (Kept at the end for auditing/transparency)
    edu_dev_revex, edu_nondev_revex, edu_dev_capex_outlay, edu_dev_capex_loan,
    health_dev_revex, health_nondev_revex, health_dev_capex_outlay, health_dev_capex_loan,
    nut_dev_revex
  ) %>%
  arrange(state, year)

# ---------------------------------------------------------
# 5. Filter for Target States
# ---------------------------------------------------------
selected_states_df <- master_df %>%
  filter(state %in% target_states)

cat("\n[SUCCESS] Data processing complete.\n")

[SUCCESS] Data processing complete.
Show R Code
# =========================================================
# PLOTTING SECTION
# =========================================================

# Prepare plot data with numeric year for continuous X-axis scaling
plot_data <- selected_states_df %>%
  mutate(year_num = as.numeric(str_extract(year, "^\\d{4}")))

# ---------------------------------------------------------
# MODIFIED THEME: Scaled down text to fit long captions/titles
# ---------------------------------------------------------
theme_publication <- function() {
  theme_bw() +
    theme(
      plot.title       = element_text(hjust = 0.5, face = "bold", size = 14), # Reduced from 18 to 14
      panel.border     = element_rect(colour = "black", fill = NA, linewidth = 1.2), # Thinned border slightly
      axis.title       = element_text(face = "bold", size = 12), # Reduced from 14 to 12
      axis.text        = element_text(face = "bold", size = 10, color = "black"), # Reduced from 12 to 10
      axis.text.x      = element_text(angle = 45, hjust = 1),
      plot.caption     = element_text(face = "bold", hjust = 0, size = 8.5, lineheight = 1.1), # Reduced from 10 to 8.5 to fit long URLs
      strip.background = element_rect(fill = "grey90", color = "black", linewidth = 1),
      strip.text       = element_text(face = "bold", size = 11),
      legend.position  = "top",
      legend.title     = element_text(face = "bold", size = 11),
      legend.text      = element_text(size = 11),
      
      # CRITICAL FIX: Align title and caption to the full plot width, not just the panel
      plot.title.position = "plot",
      plot.caption.position = "plot",
      
      # Unified padding
      plot.margin      = margin(t = 10, r = 10, b = 15, l = 10)
    )
}

# ---------------------------------------------------------
# 1. Education Expenditure Plot
# ---------------------------------------------------------

edu_only_rbi_gsdp_caption = "Source: Education Sector Expenditure - Education, Sports, Art and Culture [I.A.1 + II.C.4.i + I.1.a.1 + IV.1.a.1]\nRBI State Finances (eSTATES Database), GSDP - MoSPI."



edu_only_rbi_totexp_caption = "Source: RBI State Finances (eSTATES Database).\nEducation Sector - Education, Sports, Art and Culture [I.A.1 + II.C.4.i + I.1.a.1 + IV.1.a.1]."

p_edu <- ggplot(plot_data, aes(x = year_num, y = pct_totexp_tot_edu, color = state, group = state)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_x_continuous(breaks = unique(plot_data$year_num), labels = unique(plot_data$year)) +
  scale_y_continuous(labels = function(x) paste0(round(x, 1), "%")) +
  labs(
    title = "Education Sector Expenditure as a Percentage of Total State Expenditure",
    x = "Financial Year",
    y = "Percentage of Total Expenditure",
    color = "State",
    caption = str_wrap(edu_only_rbi_totexp_caption, width = 130)
  ) +
  theme_publication()

print(p_edu)

Show R Code
# ---------------------------------------------------------
# 2. Health Expenditure Plot
# ---------------------------------------------------------
health_only_rbi_gsdp_caption = "Source: RBI State Finances (eSTATES Database), GSDP - MoSPI.\nHealth Sector - Medical and Public Health [I.A.2 II.C.4.ii I.1.a.2 IV.1.a.2] & Family Welfare [I.A.3 II.C.4.iii I.1.a.3 IV.1.a.3]."


 

health_only_rbi_totexp_caption = "Source: RBI State Finances (eSTATES Database).\nHealth Sector - Medical and Public Health [I.A.2 II.C.4.ii I.1.a.2 IV.1.a.2] & Family Welfare [I.A.3 II.C.4.iii I.1.a.3 IV.1.a.3]."

p_health <- ggplot(plot_data, aes(x = year_num, y = pct_totexp_tot_health, color = state, group = state)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_x_continuous(breaks = unique(plot_data$year_num), labels = unique(plot_data$year)) +
  scale_y_continuous(labels = function(x) paste0(round(x, 1), "%")) +
  labs(
    title = "Health Sector Expenditure as a Percentage of Total State Expenditure",
    x = "Financial Year",
    y = "Percentage of Total Expenditure",
    color = "State",
    caption = str_wrap(health_only_rbi_totexp_caption, width = 130)
  ) +
  theme_publication()

print(p_health)

Show R Code
# ---------------------------------------------------------
# 3. Nutrition Expenditure Plot
# ---------------------------------------------------------
nut_only_rbi_gsdp_caption = "Source: Nutrition Sector [I.A.10] - RBI State Finances (eSTATES Database), GSDP - MoSPI."
nut_only_rbi_totexp_caption = "Source: Nutrition Sector Expenditure [I.A.10] - RBI State Finances (eSTATES Database)."

p_nut <- ggplot(plot_data, aes(x = year_num, y = pct_totexp_tot_nut, color = state, group = state)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_x_continuous(breaks = unique(plot_data$year_num), labels = unique(plot_data$year)) +
  scale_y_continuous(labels = function(x) paste0(round(x, 2), "%")) +
  labs(
    title = "Nutrition Sector Expenditure as a Percentage of Total State Expenditure",
    x = "Financial Year",
    y = "Percentage of Total Expenditure",
    color = "State",
    caption = str_wrap(nut_only_rbi_totexp_caption, width = 130)
  ) +
  theme_publication()

print(p_nut)

2.4 Between States: Sectoral Expenditure as a Percentage of Total State Expenditure

Show R Code
# ==============================================================================
# 1. SETUP & LIBRARIES
# ==============================================================================
library(tidyverse)
library(janitor)
library(readxl)
library(stringr)

# ---------------------------------------------------------
# Define Paths & Helpers
# ---------------------------------------------------------
rbi_path  <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/rbiestates/ESTATES23012026.XLSX"
gsdp_path <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/longdf/Bernstein_Master_GVA_GSDP_Long.csv"

target_states <- c("Karnataka", "Tamil Nadu", "Maharashtra", "Odisha")

fix_states <- function(x) {
  case_when(
    str_detect(x, "(?i)Delhi") ~ "Delhi",
    str_detect(x, "(?i)Jammu") ~ "Jammu & Kashmir",
    str_detect(x, "(?i)Andaman") ~ "Andaman & Nicobar Islands",
    TRUE ~ str_trim(x)
  )
}

# ==============================================================================
# 2. RAW DATA INGESTION & CLEANING
# ==============================================================================
rbi_raw  <- read_excel(rbi_path, sheet = "Data") %>% clean_names()
gsdp_raw <- read_csv(gsdp_path, show_col_types = FALSE) %>% clean_names()

gsdp_curr <- gsdp_raw %>%
  mutate(state = fix_states(state), year = as.character(year)) %>%
  filter(metric == 'GSDP', price_base == "Current") %>%
  select(state, year, gsdp_cr = value)

rbi_clean <- rbi_raw %>%
  rename(state = state_ut, year = fiscal_year) %>%
  mutate(
    state = fix_states(state),
    year = str_replace(year, "^(\\d{4})-\\d{2}(\\d{2})$", "\\1-\\2"),
    exp_cr = case_when(account > 0 ~ account, revised > 0 ~ revised, budget > 0 ~ budget, TRUE ~ NA_real_)
  ) %>%
  filter(!is.na(exp_cr))

# ==============================================================================
# 3. MAPPING RBI BUDGET HEADS & CREATING MASTER_DF_COMPLETE
# ==============================================================================
rbi_metrics <- rbi_clean %>%
  mutate(
    var_name = case_when(
      appendix == "Appendix-2" & budget_head == "Total: TOTAL EXPENDITURE (I+II+III)" ~ "state_total_revex",
      appendix == "Appendix-4" & budget_head == "Total$: TOTAL CAPITAL DISBURSEMENTS (Excluding Public Accounts)" ~ "state_total_capex",
      
      appendix == "Appendix-2" & budget_head == "I.A.1: Education, Sports, Art and Culture" ~ "edu_dev_revex",
      appendix == "Appendix-2" & budget_head == "II.C.4.i: Education, Sports, Art and Culture" ~ "edu_nondev_revex", 
      appendix == "Appendix-4" & budget_head == "I.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head == "IV.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_loan",

      appendix == "Appendix-2" & budget_head %in% c("I.A.2: Medical and Public Health", "I.A.3: Family Welfare") ~ "health_dev_revex",
      appendix == "Appendix-2" & budget_head %in% c("II.C.4.ii: Medical and Public Health", "II.C.4.iii: Family Welfare") ~ "health_nondev_revex",
      appendix == "Appendix-4" & budget_head %in% c("I.1.a.2: Medical and Public Health", "I.1.a.3: Family Welfare") ~ "health_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head %in% c("IV.1.a.2: Medical and Public Health", "IV.1.a.3: Family Welfare") ~ "health_dev_capex_loan",

      appendix == "Appendix-2" & budget_head == "I.A.10: Nutrition" ~ "nut_dev_revex",
      TRUE ~ NA_character_
    )
  ) %>%
  filter(!is.na(var_name)) %>%
  group_by(state, year, var_name) %>%
  summarise(exp = sum(exp_cr, na.rm = TRUE), .groups = "drop") %>%
  pivot_wider(names_from = var_name, values_from = exp, values_fill = 0)

# Safety check: Ensure all columns exist before calculating totals
cols_needed <- c(
  "state_total_revex", "state_total_capex",
  "edu_dev_revex", "edu_nondev_revex", "edu_dev_capex_outlay", "edu_dev_capex_loan",
  "health_dev_revex", "health_nondev_revex", "health_dev_capex_outlay", "health_dev_capex_loan",
  "nut_dev_revex"
)
for(col in cols_needed) { if(!col %in% names(rbi_metrics)) rbi_metrics[[col]] <- 0 }

# Merge with GSDP and calculate final overarching metrics
master_df_complete <- gsdp_curr %>%
  left_join(rbi_metrics, by = c("state", "year")) %>%
  mutate(
    year_num    = as.numeric(str_extract(year, "^\\d{4}")),
    total_revex = state_total_revex,
    total_capex = state_total_capex,
    total_exp   = total_revex + total_capex,
    
    edu_revex   = edu_dev_revex + edu_nondev_revex,
    edu_capex   = edu_dev_capex_outlay + edu_dev_capex_loan,
    tot_edu_exp = edu_revex + edu_capex,
    
    health_revex   = health_dev_revex + health_nondev_revex,
    health_capex   = health_dev_capex_outlay + health_dev_capex_loan,
    tot_health_exp = health_revex + health_capex,
    
    nut_revex   = nut_dev_revex,
    nut_capex   = 0,
    tot_nut_exp = nut_revex + nut_capex
  )

# ==============================================================================
# 4. WITHIN-STATE PERCENTAGE CALCULATIONS & RESHAPING
# ==============================================================================
selected_states_df_complete <- master_df_complete %>% 
  filter(state %in% target_states) %>%
  mutate(
    pct_totexp_edu    = (tot_edu_exp / total_exp) * 100,
    pct_totexp_health = (tot_health_exp / total_exp) * 100,
    pct_totexp_nut    = (tot_nut_exp / total_exp) * 100,
    
    pct_revex_edu     = (edu_revex / total_revex) * 100,
    pct_revex_health  = (health_revex / total_revex) * 100,
    pct_revex_nut     = (nut_revex / total_revex) * 100,
    
    pct_capex_edu     = if_else(total_capex > 0, (edu_capex / total_capex) * 100, 0),
    pct_capex_health  = if_else(total_capex > 0, (health_capex / total_capex) * 100, 0),
    pct_capex_nut     = if_else(total_capex > 0, (nut_capex / total_capex) * 100, 0)
  )

within_state_long <- selected_states_df_complete %>%
  select(
    state, year, year_num,
    `Total Exp_Education` = pct_totexp_edu, `Total Exp_Health` = pct_totexp_health, `Total Exp_Nutrition` = pct_totexp_nut,
    `RevEx_Education`     = pct_revex_edu,  `RevEx_Health`     = pct_revex_health,  `RevEx_Nutrition`     = pct_revex_nut,
    `CapEx_Education`     = pct_capex_edu,  `CapEx_Health`     = pct_capex_health,  `CapEx_Nutrition`     = pct_capex_nut
  ) %>%
  pivot_longer(
    cols = -c(state, year, year_num), # CRITICAL FIX: Explicitly ignore ID columns
    names_to = c("Category", "Sector"),
    names_sep = "_",
    values_to = "pct_value"
  ) %>%
  mutate(Sector = factor(Sector, levels = c("Education", "Health", "Nutrition")))

# ==============================================================================
# 5. THEME & PLOTTING LOOP
# ==============================================================================

c <- "Source: RBI State Finances (eSTATES Database), GSDP - MoSPI.\nEducation: Education, Sports, Art and Culture [I.A.1 + II.C.4.i + I.1.a.1 + IV.1.a.1]. Nutrition [I.A.10].\nHealth: Medical and Public Health [I.A.2 + II.C.4.ii + I.1.a.2 + IV.1.a.2] & Family Welfare [I.A.3 + II.C.4.iii + I.1.a.3 + IV.1.a.3]."

# Reusing the modified theme that prevents clipping
theme_publication <- function() {
  theme_bw() +
    theme(
      plot.title       = element_text(hjust = 0.5, face = "bold", size = 14), 
      panel.border     = element_rect(colour = "black", fill = NA, linewidth = 1.2), 
      axis.title       = element_text(face = "bold", size = 12), 
      axis.text        = element_text(face = "bold", size = 10, color = "black"), 
      axis.text.x      = element_text(angle = 45, hjust = 1),
      plot.caption     = element_text(face = "bold", hjust = 0, size = 8.5, lineheight = 1.1), 
      strip.background = element_rect(fill = "grey90", color = "black", linewidth = 1),
      strip.text       = element_text(face = "bold", size = 11),
      legend.position  = "top",
      legend.title     = element_text(face = "bold", size = 11),
      legend.text      = element_text(size = 11),
      
      # CRITICAL FIX: Align title and caption to the full plot width
      plot.title.position = "plot",
      plot.caption.position = "plot",
      
      # Unified padding
      plot.margin      = margin(t = 10, r = 10, b = 15, l = 10)
    )
}






# Loop over states and generate the Total Expenditure Share plots
for (st in target_states) {
  
  df_state <- within_state_long %>% filter(state == st)
  
  if(nrow(df_state) == 0) next
  
  p <- ggplot(df_state %>% filter(Category == "Total Exp"), aes(x = year_num, y = pct_value, color = Sector, group = Sector)) +
    geom_line(linewidth = 1.2) + 
    geom_point(size = 3) +
    scale_x_continuous(breaks = unique(df_state$year_num), labels = unique(df_state$year)) +
    scale_y_continuous(labels = function(x) paste0(round(x, 2), "%")) +
    scale_color_manual(values = c("Education" = "#2ca02c", "Health" = "#ff7f0e", "Nutrition" = "#d62728")) +
    labs(
      title = str_wrap(paste0(st, ": Sectoral Expenditure as a Percentage of Total State Expenditure"), width = 90),
      x = "Financial Year", 
      y = "Percentage of Total Expenditure", 
      color = "Sector", 
      caption = c
    ) +
    theme_publication()
  
  print(p)
}

2.5 Sectoral Expenditure as a Percentage of GSDP and Total State Expenditure

Show R Code
# ==============================================================================
# 1. SETUP, LIBRARIES, AND DATA PIPELINE
# ==============================================================================
library(tidyverse)
library(janitor)
library(readxl)
library(stringr)

rbi_path  <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/rbiestates/ESTATES23012026.XLSX"
gsdp_path <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/longdf/Bernstein_Master_GVA_GSDP_Long.csv"
target_states <- c("Karnataka", "Tamil Nadu", "Maharashtra", "Odisha")

fix_states <- function(x) {
  case_when(
    str_detect(x, "(?i)Delhi") ~ "Delhi",
    str_detect(x, "(?i)Jammu") ~ "Jammu & Kashmir",
    str_detect(x, "(?i)Andaman") ~ "Andaman & Nicobar Islands",
    TRUE ~ str_trim(x)
  )
}

rbi_raw  <- read_excel(rbi_path, sheet = "Data") %>% clean_names()
gsdp_raw <- read_csv(gsdp_path, show_col_types = FALSE) %>% clean_names()

gsdp_curr <- gsdp_raw %>%
  mutate(state = fix_states(state), year = as.character(year)) %>%
  filter(metric == 'GSDP', price_base == "Current") %>%
  select(state, year, gsdp_cr = value)

rbi_clean <- rbi_raw %>%
  rename(state = state_ut, year = fiscal_year) %>%
  mutate(
    state = fix_states(state),
    year = str_replace(year, "^(\\d{4})-\\d{2}(\\d{2})$", "\\1-\\2"),
    exp_cr = case_when(account > 0 ~ account, revised > 0 ~ revised, budget > 0 ~ budget, TRUE ~ NA_real_)
  ) %>%
  filter(!is.na(exp_cr))

rbi_metrics <- rbi_clean %>%
  mutate(
    var_name = case_when(
      appendix == "Appendix-2" & budget_head == "Total: TOTAL EXPENDITURE (I+II+III)" ~ "state_total_revex",
      appendix == "Appendix-4" & budget_head == "Total$: TOTAL CAPITAL DISBURSEMENTS (Excluding Public Accounts)" ~ "state_total_capex",
      appendix == "Appendix-2" & budget_head == "I.A.1: Education, Sports, Art and Culture" ~ "edu_dev_revex",
      appendix == "Appendix-2" & budget_head == "II.C.4.i: Education, Sports, Art and Culture" ~ "edu_nondev_revex", 
      appendix == "Appendix-4" & budget_head == "I.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head == "IV.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_loan",
      appendix == "Appendix-2" & budget_head %in% c("I.A.2: Medical and Public Health", "I.A.3: Family Welfare") ~ "health_dev_revex",
      appendix == "Appendix-2" & budget_head %in% c("II.C.4.ii: Medical and Public Health", "II.C.4.iii: Family Welfare") ~ "health_nondev_revex",
      appendix == "Appendix-4" & budget_head %in% c("I.1.a.2: Medical and Public Health", "I.1.a.3: Family Welfare") ~ "health_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head %in% c("IV.1.a.2: Medical and Public Health", "IV.1.a.3: Family Welfare") ~ "health_dev_capex_loan",
      appendix == "Appendix-2" & budget_head == "I.A.10: Nutrition" ~ "nut_dev_revex",
      TRUE ~ NA_character_
    )
  ) %>%
  filter(!is.na(var_name)) %>%
  group_by(state, year, var_name) %>%
  summarise(exp = sum(exp_cr, na.rm = TRUE), .groups = "drop") %>%
  pivot_wider(names_from = var_name, values_from = exp, values_fill = 0)

cols_needed <- c("state_total_revex", "state_total_capex", "edu_dev_revex", "edu_nondev_revex", "edu_dev_capex_outlay", "edu_dev_capex_loan", "health_dev_revex", "health_nondev_revex", "health_dev_capex_outlay", "health_dev_capex_loan", "nut_dev_revex")
for(col in cols_needed) { if(!col %in% names(rbi_metrics)) rbi_metrics[[col]] <- 0 }

master_df_complete <- gsdp_curr %>%
  left_join(rbi_metrics, by = c("state", "year")) %>%
  mutate(
    year_num    = as.numeric(str_extract(year, "^\\d{4}")),
    total_revex = state_total_revex,
    total_capex = state_total_capex,
    total_exp   = total_revex + total_capex,
    edu_revex   = edu_dev_revex + edu_nondev_revex,
    edu_capex   = edu_dev_capex_outlay + edu_dev_capex_loan,
    tot_edu_exp = edu_revex + edu_capex,
    health_revex   = health_dev_revex + health_nondev_revex,
    health_capex   = health_dev_capex_outlay + health_dev_capex_loan,
    tot_health_exp = health_revex + health_capex,
    nut_revex   = nut_dev_revex,
    nut_capex   = 0,
    tot_nut_exp = nut_revex + nut_capex
  ) %>% filter(state %in% target_states)

# ==============================================================================
# 2. CALCULATE METRICS FOR DUAL AXIS
# ==============================================================================
plot_data <- master_df_complete %>%
  mutate(
    # Primary Axis Metrics (% GSDP)
    gsdp_edu    = (tot_edu_exp / gsdp_cr) * 100,
    gsdp_health = (tot_health_exp / gsdp_cr) * 100,
    gsdp_nut    = (tot_nut_exp / gsdp_cr) * 100,
    
    # Secondary Axis Metrics (% Total Expenditure)
    totexp_edu    = (tot_edu_exp / total_exp) * 100,
    totexp_health = (tot_health_exp / total_exp) * 100,
    totexp_nut    = (tot_nut_exp / total_exp) * 100,
    
    # Yearly Ratio (Total Exp / GSDP) used to calculate the state's average scaling factor
    yearly_ratio = total_exp / gsdp_cr
  )

# ==============================================================================
# 3. LOOP TO GENERATE 1 PLOT PER STATE
# ==============================================================================
for (st in target_states) {
  
  df_state <- plot_data %>% filter(state == st)
  if(nrow(df_state) == 0) next
  
  # Calculate the single static scalar required for the secondary axis for this specific state
  avg_ratio <- mean(df_state$yearly_ratio, na.rm = TRUE)
  
  p <- ggplot(df_state, aes(x = year_num)) +
    
    # --- EDUCATION (Green) ---
    geom_line(aes(y = gsdp_edu, color = "Education", linetype = "% of GSDP"), linewidth = 1.2) +
    geom_point(aes(y = gsdp_edu, color = "Education", shape = "% of GSDP"), size = 3) +
    
    geom_line(aes(y = totexp_edu * avg_ratio, color = "Education", linetype = "% of Total Exp"), linewidth = 1.2) +
    geom_point(aes(y = totexp_edu * avg_ratio, color = "Education", shape = "% of Total Exp"), size = 3) +

    # --- HEALTH (Orange) ---
    geom_line(aes(y = gsdp_health, color = "Health", linetype = "% of GSDP"), linewidth = 1.2) +
    geom_point(aes(y = gsdp_health, color = "Health", shape = "% of GSDP"), size = 3) +
    
    geom_line(aes(y = totexp_health * avg_ratio, color = "Health", linetype = "% of Total Exp"), linewidth = 1.2) +
    geom_point(aes(y = totexp_health * avg_ratio, color = "Health", shape = "% of Total Exp"), size = 3) +

    # --- NUTRITION (Red) ---
    geom_line(aes(y = gsdp_nut, color = "Nutrition", linetype = "% of GSDP"), linewidth = 1.2) +
    geom_point(aes(y = gsdp_nut, color = "Nutrition", shape = "% of GSDP"), size = 3) +
    
    geom_line(aes(y = totexp_nut * avg_ratio, color = "Nutrition", linetype = "% of Total Exp"), linewidth = 1.2) +
    geom_point(aes(y = totexp_nut * avg_ratio, color = "Nutrition", shape = "% of Total Exp"), size = 3) +
    
    # --- SCALES & AXES ---
    scale_x_continuous(breaks = unique(df_state$year_num), labels = unique(df_state$year)) +
    
    scale_y_continuous(
      name = "Percentage of GSDP (Solid Line, Circles)",
      labels = function(x) paste0(round(x, 2), "%"),
      sec.axis = sec_axis(
        transform = ~ . / avg_ratio, 
        name = "Percentage of Total Expenditure (Dashed Line, Triangles)",
        labels = function(x) paste0(round(x, 1), "%")
      )
    ) +
    
    # --- AESTHETICS ---
    scale_color_manual(values = c("Education" = "#2ca02c", "Health" = "#ff7f0e", "Nutrition" = "#d62728")) +
    scale_linetype_manual(values = c("% of GSDP" = "solid", "% of Total Exp" = "dashed")) +
    scale_shape_manual(values = c("% of GSDP" = 16, "% of Total Exp" = 17)) +
    
    labs(
      title = str_wrap(paste0(st, ": Sectoral Expenditure as Percentage of GSDP and Total Expenditure"), width = 90),
      x = "Financial Year",
      color = "Sector",
      linetype = "Metric",
      shape = "Metric"
    ) +
    theme_bw() +
    theme(
      plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
      axis.title.y.left = element_text(face = "bold", size = 11),
      axis.title.y.right = element_text(face = "bold", size = 11),
      axis.text = element_text(face = "bold", color = "black"),
      axis.text.x = element_text(angle = 45, hjust = 1),
      legend.position = "top",
      legend.box = "vertical",
      plot.margin = margin(t = 10, r = 10, b = 15, l = 10)
    )
  
  print(p)
}

3 UDISE - Expenditure Analysis

Show R Code
#| label: udise-expenditure-unit-metrics
#| fig-width: 12
#| fig-height: 7
#| out-width: "100%"
#| message: false
#| warning: false

# ==============================================================================
# 1. SETUP & LIBRARIES
# ==============================================================================
library(tidyverse)
library(janitor)
library(readxl)
library(stringr)
library(scales) 

# ---------------------------------------------------------
# Define Paths & Helpers
# ---------------------------------------------------------
rbi_path  <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/rbiestates/ESTATES23012026.XLSX"
gsdp_path <- "C:/Users/CEGIS/Desktop/work/sectoral_scan/r_gva/data/2026_07_31_cleaned/longdf/Bernstein_Master_GVA_GSDP_Long.csv"

target_states <- c("Karnataka", "Tamil Nadu", "Maharashtra", "Odisha")

fix_states <- function(x) {
  case_when(
    str_detect(x, "(?i)Delhi") ~ "Delhi",
    str_detect(x, "(?i)Jammu") ~ "Jammu & Kashmir",
    str_detect(x, "(?i)Andaman") ~ "Andaman & Nicobar Islands",
    TRUE ~ str_trim(x)
  )
}

# ---------------------------------------------------------
# MODIFIED THEME (Prevents clipping of long text)
# ---------------------------------------------------------
theme_publication1 <- function() {
  theme_bw() +
    theme(
      plot.title       = element_text(hjust = 0.5, face = "bold", size = 14), 
      panel.border     = element_rect(colour = "black", fill = NA, linewidth = 1.2), 
      axis.title       = element_text(face = "bold", size = 12), 
      axis.text        = element_text(face = "bold", size = 10, color = "black"), 
      axis.text.x      = element_text(angle = 0, hjust = 0.5), # Kept at 0 per your original theme
      plot.caption     = element_text(face = "bold", hjust = 0, size = 8.5, lineheight = 1.1), 
      legend.position  = "top",
      legend.title     = element_text(face = "bold", size = 11),
      legend.text      = element_text(size = 11),
      
      # CRITICAL FIX: Align title and caption to the full plot width
      plot.title.position = "plot",
      plot.caption.position = "plot",
      
      # Unified padding
      plot.margin      = margin(t = 10, r = 10, b = 15, l = 10)
    )
}

# ==============================================================================
# 2. RAW DATA INGESTION & CLEANING
# ==============================================================================
rbi_raw  <- read_excel(rbi_path, sheet = "Data") %>% clean_names()
gsdp_raw <- read_csv(gsdp_path, show_col_types = FALSE) %>% clean_names()

gsdp_curr <- gsdp_raw %>%
  mutate(state = fix_states(state), year = as.character(year)) %>%
  filter(metric == 'GSDP', price_base == "Current") %>%
  select(state, year, gsdp_cr = value)

rbi_clean <- rbi_raw %>%
  rename(state = state_ut, year = fiscal_year) %>%
  mutate(
    state = fix_states(state),
    year = str_replace(year, "^(\\d{4})-\\d{2}(\\d{2})$", "\\1-\\2"),
    exp_cr = case_when(account > 0 ~ account, revised > 0 ~ revised, budget > 0 ~ budget, TRUE ~ NA_real_)
  ) %>%
  filter(!is.na(exp_cr))

# ==============================================================================
# 3. MAPPING RBI BUDGET HEADS & CREATING MASTER_DF_COMPLETE
# ==============================================================================
rbi_metrics <- rbi_clean %>%
  mutate(
    var_name = case_when(
      appendix == "Appendix-2" & budget_head == "Total: TOTAL EXPENDITURE (I+II+III)" ~ "state_total_revex",
      appendix == "Appendix-4" & budget_head == "Total$: TOTAL CAPITAL DISBURSEMENTS (Excluding Public Accounts)" ~ "state_total_capex",
      appendix == "Appendix-2" & budget_head == "I.A.1: Education, Sports, Art and Culture" ~ "edu_dev_revex",
      appendix == "Appendix-2" & budget_head == "II.C.4.i: Education, Sports, Art and Culture" ~ "edu_nondev_revex", 
      appendix == "Appendix-4" & budget_head == "I.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_outlay",
      appendix == "Appendix-4" & budget_head == "IV.1.a.1: Education, Sports, Art and Culture" ~ "edu_dev_capex_loan",
      TRUE ~ NA_character_
    )
  ) %>%
  filter(!is.na(var_name)) %>%
  group_by(state, year, var_name) %>%
  summarise(exp = sum(exp_cr, na.rm = TRUE), .groups = "drop") %>%
  pivot_wider(names_from = var_name, values_from = exp, values_fill = 0)

cols_needed <- c("state_total_revex", "state_total_capex", "edu_dev_revex", "edu_nondev_revex", "edu_dev_capex_outlay", "edu_dev_capex_loan")
for(col in cols_needed) { if(!col %in% names(rbi_metrics)) rbi_metrics[[col]] <- 0 }

master_df_complete <- gsdp_curr %>%
  left_join(rbi_metrics, by = c("state", "year")) %>%
  mutate(
    year_num = as.numeric(str_extract(year, "^\\d{4}")),
    total_revex = state_total_revex,
    total_capex = state_total_capex,
    total_exp   = total_revex + total_capex,
    edu_revex   = edu_dev_revex + edu_nondev_revex,
    edu_capex   = edu_dev_capex_outlay + edu_dev_capex_loan,
    tot_edu_exp = edu_revex + edu_capex
  ) %>%
  filter(state %in% target_states)

# ==============================================================================
# 4. LOAD UDISE DATA & CALCULATE UNIT-LEVEL METRICS
# ==============================================================================
udise_wide <- read_csv("udise_sp_wide.csv", show_col_types = FALSE)

edu_metrics_df <- master_df_complete %>%
  inner_join(
    udise_wide %>% rename(year = fiscal_year), 
    by = c("state", "year")
  ) %>%
  mutate(
    # A. Convert RBI Crores to Actual Rupees (1 Crore = 10,000,000)
    edu_dev_revex_rs    = edu_dev_revex * 10000000,
    edu_total_revex_rs  = edu_revex * 10000000, 
    edu_capex_outlay_rs = edu_dev_capex_outlay * 10000000, 
    tot_edu_rs          = (edu_revex + edu_capex) * 10000000,
    
    # B. Plot Metrics
    monthly_cost_per_teacher_r = (edu_total_revex_rs / total_teachers) / 12,
    monthly_cost_per_teacher   = (edu_dev_revex_rs / total_teachers) / 12, # Used for filtering logic
    annual_revex_per_child     = edu_total_revex_rs / total_enrolment,
    annual_capex_per_school    = edu_capex_outlay_rs / total_schools,
    annual_revex_per_school    = edu_total_revex_rs / total_schools,
    annual_total_per_school    = tot_edu_rs / total_schools
  ) %>%
  filter(!is.na(monthly_cost_per_teacher))

# ==============================================================================
# 5. GENERATE PLOTS
# ==============================================================================

# --- Plot 1: Monthly Cost Per Teacher ---
cap_teacher <- "Source: UDISE+ & RBI State Finances: Education, sports, Art and Culture [I.A.1].\nRevenue Expenditure (Government spending on recurring, day-to-day operational costs-such as salaries, pensions\nand maintenance)."

p_teacher <- ggplot(edu_metrics_df, aes(x = year_num, y = monthly_cost_per_teacher_r, color = state, group = state)) +
  geom_line(linewidth = 1.2) + geom_point(size = 3) +
  scale_x_continuous(breaks = unique(edu_metrics_df$year_num), labels = unique(edu_metrics_df$year)) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title   = str_wrap("Monthly Revenue Expenditure per Teacher", width = 90),
    x       = "Financial Year",
    y       = "Expenditure (in Rs)",
    color   = "State",
    caption = cap_teacher
  ) +
  theme_publication1()

print(p_teacher)

Show R Code
# --- Plot 2: Annual RevEx Per Child ---
cap_child <- "Source: UDISE+ & RBI State Finances: Education, sports, Art and Culture [I.A.1 + II.C.4.i].\nRevenue Expenditure (Government spending on recurring, day-to-day operational costs-such as salaries, pensions\nand maintenance)."

p_child <- ggplot(edu_metrics_df, aes(x = year_num, y = annual_revex_per_child, color = state, group = state)) +
  geom_line(linewidth = 1.2) + geom_point(size = 3) +
  scale_x_continuous(breaks = unique(edu_metrics_df$year_num), labels = unique(edu_metrics_df$year)) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title   = str_wrap("Annual Revenue Expenditure per Enrolled Child", width = 90),
    x       = "Financial Year",
    y       = "Expenditure (in Rs)",
    color   = "State",
    caption = cap_child
  ) +
  theme_publication1()

print(p_child)

Show R Code
# --- Plot 3: Annual CapEx Per School ---
cap_school_cap <- "Source: UDISE+ & RBI State Finances: Education, sports, Art and Culture [I.1.a.1].\nCapital Expenditure (Government spending utilized to create permanent physical and financial assets)."

p_school_cap <- ggplot(edu_metrics_df, aes(x = year_num, y = annual_capex_per_school, color = state, group = state)) +
  geom_line(linewidth = 1.2) + geom_point(size = 3) +
  scale_x_continuous(breaks = unique(edu_metrics_df$year_num), labels = unique(edu_metrics_df$year)) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title   = str_wrap("Annual Capital Infrastructure Expenditure per School", width = 90),
    x       = "Financial Year",
    y       = "Expenditure (in Rs)",
    color   = "State",
    caption = cap_school_cap
  ) +
  theme_publication1()

print(p_school_cap)

Show R Code
# --- Plot 4: Annual RevEx Per School ---
cap_school_rev <- "Source: UDISE+ & RBI State Finances: Education, sports, Art and Culture [I.A.1 + II.C.4.i].\nRevenue Expenditure (Government spending on recurring, day-to-day operational costs-such as salaries, pensions\nand maintenance)."

p_school_rev <- ggplot(edu_metrics_df, aes(x = year_num, y = annual_revex_per_school, color = state, group = state)) +
  geom_line(linewidth = 1.2) + geom_point(size = 3) +
  scale_x_continuous(breaks = unique(edu_metrics_df$year_num), labels = unique(edu_metrics_df$year)) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title   = str_wrap("Annual Revenue Expenditure per School", width = 90),
    x       = "Financial Year",
    y       = "Expenditure (in Rs)",
    color   = "State",
    caption = cap_school_rev
  ) +
  theme_publication1()

print(p_school_rev)

Show R Code
# --- Plot 5: Annual Total Expenditure Per School ---
cap_school_tot <- "Source: UDISE+ & RBI State Finances: Education, sports, Art and Culture [I.A.1 + II.C.4.i + I.1.a.1 + IV.1.a.1].\nTotal Expenditure (Revenue + Capital Expenditure) captures government spending on both operational and\ninfrastructure cost)."

p_school_tot <- ggplot(edu_metrics_df, aes(x = year_num, y = annual_total_per_school, color = state, group = state)) +
  geom_line(linewidth = 1.2) + 
  geom_point(size = 3) +
  scale_x_continuous(breaks = unique(edu_metrics_df$year_num), labels = unique(edu_metrics_df$year)) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title   = str_wrap("Annual Total Expenditure per School", width = 90),
    x       = "Financial Year",
    y       = "Expenditure (in Rs)",
    color   = "State",
    caption = cap_school_tot
  ) +
  theme_publication1()

print(p_school_tot)

4 FY 2023-24 Analysis

Show R Code
#| label: fy23-24-stacked-bar-composition
#| fig-width: 12
#| fig-height: 7
#| out-width: "100%"
#| message: false
#| warning: false

library(tidyverse)
library(stringr)

# ---------------------------------------------------------
# 1. Setup Theme (to prevent text clipping)
# ---------------------------------------------------------
theme_publication <- function() {
  theme_bw() +
    theme(
      plot.title       = element_text(hjust = 0.5, face = "bold", size = 14),
      panel.border     = element_rect(colour = "black", fill = NA, linewidth = 1.2),
      axis.title       = element_text(face = "bold", size = 12),
      axis.text        = element_text(face = "bold", size = 10, color = "black"),
      axis.text.x      = element_text(angle = 45, hjust = 1),
      plot.caption     = element_text(face = "bold", hjust = 0, size = 8.5, lineheight = 1.1),
      legend.position  = "top",
      legend.title     = element_text(face = "bold", size = 11),
      legend.text      = element_text(size = 11),
      plot.title.position = "plot",
      plot.caption.position = "plot",
      plot.margin      = margin(t = 10, r = 10, b = 15, l = 10)
    )
}

# ---------------------------------------------------------
# 2. Generate the Table for FY 2023-24
# ---------------------------------------------------------
table_23_24 <- master_df %>%
  filter(year == "2023-24", state %in% target_states) %>%
  select(
    State = state,
    GSDP_Current = gsdp_cr,
    Total_Exp = total_exp,
    Education_Exp = tot_edu_exp,
    Health_Exp = tot_health_exp,
    Nutrition_Exp = tot_nut_exp
  )

# ---------------------------------------------------------
# 3. Prepare Data for Stacked Bar Chart (% of TOTAL EXPENDITURE)
# ---------------------------------------------------------
plot_data_23_24 <- table_23_24 %>%
  mutate(
    # Calculate each sector as a % of Total Expenditure
    Share_Edu    = (Education_Exp / Total_Exp) * 100,
    Share_Health = (Health_Exp / Total_Exp) * 100,
    Share_Nut    = (Nutrition_Exp / Total_Exp) * 100,
    Share_Other  = 100 - (Share_Edu + Share_Health + Share_Nut)
  ) %>%
  # Keep only the percentage columns for the plot
  select(State, Share_Edu, Share_Health, Share_Nut, Share_Other) %>%
  pivot_longer(
    cols = starts_with("Share_"),
    names_to = "Sector",
    values_to = "Pct_Total_Exp"
  ) %>%
  mutate(
    Sector = case_when(
      Sector == "Share_Edu"    ~ "Education",
      Sector == "Share_Health" ~ "Health",
      Sector == "Share_Nut"    ~ "Nutrition",
      Sector == "Share_Other"  ~ "Other Expenditure"
    ),
    # Lock the order so E, H, and N are at the bottom of the stack, and "Other" is at the top
    Sector = factor(Sector, levels = c("Other Expenditure", "Education" , "Health", "Nutrition"))
  )

# ---------------------------------------------------------
# 4. Plot: Composition of Total Expenditure
# ---------------------------------------------------------'
d <- "Source: RBI State Finances (eSTATES Database), GSDP - MoSPI.\nEducation: Education, Sports, Art and Culture [I.A.1 + II.C.4.i + I.1.a.1 + IV.1.a.1]. Nutrition [I.A.10].\nHealth: Medical and Public Health [I.A.2 + II.C.4.ii + I.1.a.2 + IV.1.a.2] & Family Welfare [I.A.3 + II.C.4.iii + I.1.a.3 + IV.1.a.3]."


p_stacked <- ggplot(plot_data_23_24, aes(x = State, y = Pct_Total_Exp, fill = Sector)) +
  geom_col(color = "black", linewidth = 0.5, width = 0.6) +
  
  # Add segment labels inside the bars (only for sectors > 2% to avoid text overlap)
  geom_text(
    aes(label = ifelse(Pct_Total_Exp > 2, paste0(round(Pct_Total_Exp, 1), "%"), "")),
    position = position_stack(vjust = 0.5), 
    fontface = "bold", 
    size = 3.5,
    color = "black"
  ) +
  
  # Y-axis automatically goes to 100% since it's a share of total
  scale_y_continuous(labels = function(x) paste0(x, "%"), expand = expansion(mult = c(0, 0.05))) +
  scale_fill_manual(
    values = c(
      "Education"         = "#2ca02c", # Green
      "Health"            = "#ff7f0e", # Orange
      "Nutrition"         = "#d62728", # Red
      "Other Expenditure" = "#e0e0e0"  # Light Grey to make EHN pop out
    )
  ) +
  labs(
    title = str_wrap("Sectoral Share in Total State Expenditure (FY 2023-24)", width = 90),
    x = "State",
    y = "Percentage",
    fill = "Sector",
    caption = d
  ) +
  theme_publication() +
  theme(
    axis.text.x = element_text(angle = 0, hjust = 0.5) # Override theme to keep state names horizontal
  )

print(p_stacked)