Becca Ch. 3 analysis 9.22.26

Author

R.Luttinen

Ch. 3

1. Load and perform data cleaning/ recoding

#load data
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   4.0.3     ✔ tibble    3.2.1
✔ lubridate 1.9.4     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(janitor)

Attaching package: 'janitor'

The following objects are masked from 'package:stats':

    chisq.test, fisher.test
library(ipumsr)

options(survey.lonely.psu = "adjust")

#person-period dataset

pmauglong <- read_ipums_micro(
  ddi = "C:/Users/Rebecca/Downloads/pma_00032.xml",
  data = "C:/Users/Rebecca/Downloads/pma_00032.dat.gz")
Use of data from IPUMS PMA is subject to conditions including that users should cite the data appropriately. Use command `ipums_conditions()` for more details.
#read in spatial data

library(sf)
Linking to GEOS 3.13.1, GDAL 3.10.2, PROJ 9.5.1; sf_use_s2() is TRUE
UG_PMA_GPS<-read_sf("C:/Users/Rebecca/Downloads/PMA_UG_GPS_v2_06July2022/UGANDA/PMA_UG_GPS_v2_06July2022.csv")


#read in boundary data

UG_boundary<-read_sf("C:/Users/Rebecca/Downloads/geoug/geoug.shp")


UG_boundary_4<-read_sf("C:/Users/Rebecca/Downloads/geouggen/geouggen.shp")

UG_sub_boundary<-read_sf("C:/Users/Rebecca/Downloads/uganda-adm-boundaries/UGANDA BOUNDARIES SHAPEFILES AS OF 17 08 2018/COUNTIES_2018_UTM_36N.shp")


pmaug2022<-pmauglong
#%>%
  #filter(YEAR==2021)

UG_PMA_GPS_2022<-UG_PMA_GPS
#%>%
#filter(PMAYEAR==2021)

UG_PMA_GPS_2022<-UG_PMA_GPS%>%
  rename('EAID'='EA_ID')


pmaug2022wgps<-merge(pmaug2022, UG_PMA_GPS_2022, by=c("EAID"))
#marital status 

pmaug2022wgps<-pmaug2022wgps%>%
  mutate(MARSTAT=as.factor(MARSTAT))%>%
  mutate(maritalstatus= recode(MARSTAT, '10' = "never married" ,'20' = "married or living together", '21'= "currently married", '22'= "currently living with partner", '31'="formerly in union",'32'='widow or widower',.default = NA_character_))

tabyl(pmaug2022wgps, maritalstatus)
                 maritalstatus    n      percent valid_percent
                 never married 3506 0.2472845253    0.24735431
             currently married 3416 0.2409366624    0.24100466
 currently living with partner 4980 0.3512484130    0.35134754
             formerly in union 1947 0.1373254338    0.13736419
              widow or widower  325 0.0229228382    0.02292931
                          <NA>    4 0.0002821272            NA
#age-group

pmaug2022wgps<-pmaug2022wgps %>%
  mutate(agegroup = case_when(
    AGE >= 15 & AGE <= 19 ~ "15-19",
    AGE >= 20 & AGE <= 24 ~ "20-24",
    AGE >= 25 & AGE <= 29 ~ "25-29",
    AGE >= 30 & AGE <= 34 ~ "30-34",
    AGE >= 35 & AGE <= 39 ~ "35-39",
    AGE >= 40 & AGE <= 44 ~ "40-44",
    AGE >= 45 & AGE <= 49 ~ "45-49"
  ))


pmaug2022wgps$age_z_score <- scale(pmaug2022wgps$AGE)



pmaug2022wgps<-pmaug2022wgps%>%
  filter(BIRTHEVENT<90)

pmaug2022wgps<-pmaug2022wgps %>%
  mutate(AGE=as.numeric(AGE))


pmaug2022wgps<-pmaug2022wgps %>%
  mutate(URBAN=as.factor(URBAN))

#contraceptive use

pmaug2022wgps<-pmaug2022wgps%>%
  mutate(MCP=as.factor(MCP))%>%
  mutate(usingmoderncon= recode(MCP, '1' = "yes" ,'0' = "no",.default = NA_character_))

tabyl(pmaug2022wgps, usingmoderncon)
 usingmoderncon    n    percent valid_percent
             no 9304 0.65645947     0.6662847
            yes 4660 0.32879419     0.3337153
           <NA>  209 0.01474635            NA
#education

pmaug2022wgps<-pmaug2022wgps%>%
  mutate(EDUCATTGEN=as.factor(EDUCATTGEN))%>%
  mutate(educationlevel= recode(EDUCATTGEN, '1' = "none" ,'2' = "primary/middle school", '3'= "secondary/post-primary", '4'= 'tertiary/ post-secondary',.default = NA_character_))

tabyl(pmaug2022wgps, educationlevel)
           educationlevel    n      percent valid_percent
                     none  842 0.0594087349    0.05941712
    primary/middle school 7902 0.5575389826    0.55761767
   secondary/post-primary 4268 0.3011359627    0.30117846
 tertiary/ post-secondary 1159 0.0817752064    0.08178675
                     <NA>    2 0.0001411134            NA
#collapse marital status variable

pmaug2022wgps<-pmaug2022wgps%>%
  mutate(maritalcombined= recode(maritalstatus, 'formerly in union'= 'not in a union' ,'never married' = "not in a union", 'widow or widower'= "not in a union", 'currently married'= "in a union", 'currently living with partner'="in a union"))


#make index: exercise of choice

library(janitor)


pmaug2022wgps <- pmaug2022wgps %>%
  mutate(startstop = case_when(
    STARTKIDDEC == 1 | PTRDISCKIDSTOPWILL== 1 ~ '1',
    STARTKIDDEC == 2 | PTRDISCKIDSTOPWILL== 2 ~ '2',
    STARTKIDDEC == 3 | PTRDISCKIDSTOPWILL== 3 ~ '3',
    STARTKIDDEC == 4 | PTRDISCKIDSTOPWILL== 4~ '4',
    STARTKIDDEC == 5 | PTRDISCKIDSTOPWILL== 5 ~ '5'
  ))

tabyl(pmaug2022wgps$startstop)
 pmaug2022wgps$startstop    n    percent valid_percent
                       1  598 0.04219290    0.04291045
                       2 1951 0.13765611    0.13999713
                       3  559 0.03944119    0.04011194
                       4 7395 0.52176674    0.53064007
                       5 3433 0.24222112    0.24634041
                    <NA>  237 0.01672194            NA
pmaug2022wgps <- pmaug2022wgps %>%
  mutate(havekid = case_when(
    STARTKIDDECWILL == 1 | NEXTKIDDEC== 1 ~ '1',
    STARTKIDDECWILL == 2 | NEXTKIDDEC== 2 ~ '2',
    STARTKIDDECWILL == 3 | NEXTKIDDEC== 3 ~ '3',
    STARTKIDDECWILL == 4 | NEXTKIDDEC== 4~ '4',
    STARTKIDDECWILL == 5 | NEXTKIDDEC== 5 ~ '5'
  ))


tabyl(pmaug2022wgps$havekid)
 pmaug2022wgps$havekid    n    percent valid_percent
                     1  343 0.02420095    0.02441976
                     2 1267 0.08939533    0.09020362
                     3  446 0.03146828    0.03175281
                     4 7311 0.51583998    0.52050406
                     5 4679 0.33013476    0.33311975
                  <NA>  127 0.00896070            NA
pmaug2022wgps <- pmaug2022wgps %>%
  mutate(negotiate = case_when(
    PTRDISCKIDSTOP == 1 | PTRDISCKIDSTART== 1 ~ '1',
    PTRDISCKIDSTOP == 2 | PTRDISCKIDSTART== 2 ~ '2',
    PTRDISCKIDSTOP == 3 | PTRDISCKIDSTART== 3 ~ '3',
    PTRDISCKIDSTOP == 4 | PTRDISCKIDSTART== 4~ '4',
    PTRDISCKIDSTOP == 5 | PTRDISCKIDSTART== 5 ~ '5'
  ))

tabyl(pmaug2022wgps$negotiate)
 pmaug2022wgps$negotiate    n    percent valid_percent
                       1  575 0.04057010    0.04133420
                       2 1341 0.09461652    0.09639853
                       3  591 0.04169901    0.04248436
                       4 7393 0.52162563    0.53144993
                       5 4011 0.28300289    0.28833297
                    <NA>  262 0.01848585            NA
#make index existence of choice

pmaug2022wgps <- pmaug2022wgps %>%
  mutate(restbtw = case_when(
    PREGREST == 1 ~ '1',
    PREGREST == 2 ~ '2',
    PREGREST == 3 ~ '3',
    PREGREST == 4 ~ '4',
    PREGREST == 5~ '5'
  ))

tabyl(pmaug2022wgps$restbtw)
 pmaug2022wgps$restbtw    n     percent valid_percent
                     1  127 0.008960700   0.009021809
                     2  221 0.015593029   0.015699368
                     3  150 0.010583504   0.010655679
                     4 6855 0.483666126   0.486964552
                     5 6724 0.474423199   0.477658592
                  <NA>   96 0.006773442            NA
pmaug2022wgps <- pmaug2022wgps %>%
  mutate(edufirst = case_when(
    EDUCPREKID == 1 | EDUCPREKIDWILL== 1 ~ '1',
    EDUCPREKID == 2 | EDUCPREKIDWILL== 2 ~ '2',
    EDUCPREKID == 3 | EDUCPREKIDWILL== 3 ~ '3',
    EDUCPREKID == 4 | EDUCPREKIDWILL== 4~ '4',
    EDUCPREKID == 5 | EDUCPREKIDWILL== 5 ~ '5'
  ))

tabyl(pmaug2022wgps$edufirst)
 pmaug2022wgps$edufirst    n     percent valid_percent
                      1  202 0.014252452    0.03886858
                      2  396 0.027940450    0.07619781
                      3  122 0.008607916    0.02347508
                      4 1684 0.118817470    0.32403310
                      5 2793 0.197064842    0.53742544
                   <NA> 8976 0.633316870            NA
#don't use because it isn't validated



#drop na's

pmauglongfilter2 <- pmaug2022wgps %>%
  filter(!is.na(startstop),
         !is.na(havekid),
         !is.na(negotiate))



pmauglongfilter2$startstopnum<-as.numeric(pmauglongfilter2$startstop)
pmauglongfilter2$havekidnum<-as.numeric(pmauglongfilter2$havekid)
pmauglongfilter2$negotiatenum<-as.numeric(pmauglongfilter2$negotiate)

#create summary variables: sum

pmauglongfilter2 <- pmauglongfilter2 %>%
  mutate(summary_score = (startstopnum + havekidnum + negotiatenum) )


tabyl(pmauglongfilter2$summary_score)
 pmauglongfilter2$summary_score    n     percent
                              3   67 0.004862472
                              4   68 0.004935046
                              5   98 0.007112272
                              6  342 0.024820379
                              7  273 0.019812759
                              8  632 0.045866899
                              9  514 0.037303142
                             10 1357 0.098483199
                             11  959 0.069598665
                             12 4526 0.328470861
                             13 1734 0.125843675
                             14 1330 0.096523695
                             15 1879 0.136366935
hist(pmauglongfilter2$summary_score)

#average

pmauglongfilter2 <- pmauglongfilter2 %>%
  mutate(summary_score_av = (startstopnum + havekidnum + negotiatenum)/3 )

tabyl(pmauglongfilter2$summary_score_av)
 pmauglongfilter2$summary_score_av    n     percent
                          1.000000   67 0.004862472
                          1.333333   68 0.004935046
                          1.666667   98 0.007112272
                          2.000000  342 0.024820379
                          2.333333  273 0.019812759
                          2.666667  632 0.045866899
                          3.000000  514 0.037303142
                          3.333333 1357 0.098483199
                          3.666667  959 0.069598665
                          4.000000 4526 0.328470861
                          4.333333 1734 0.125843675
                          4.666667 1330 0.096523695
                          5.000000 1879 0.136366935
hist(pmauglongfilter2$summary_score_av)

#convert to a spatial frame

pmaug2022wgps_sf<- st_as_sf(pmauglongfilter2, coords = c("GPSLONG", "GPSLAT"), crs=4326)

2. Descriptive table stratified by urban/ rural status

library(gtsummary)
library(flextable)

Attaching package: 'flextable'
The following object is masked from 'package:gtsummary':

    continuous_summary
The following object is masked from 'package:purrr':

    compose
library(haven)
library(srvyr)

Attaching package: 'srvyr'
The following object is masked from 'package:stats':

    filter
desc_design <- pmaug2022wgps_sf %>%
  st_drop_geometry() %>%
  filter(!is.na(FQWEIGHT)) %>%
  mutate(
    residence        = factor(URBAN, levels = c(0, 1), labels = c("Rural", "Urban")),
    summary_score_av = as.numeric(summary_score_av),
    BIRTHEVENT       = if_else(BIRTHEVENT >= 90, NA_real_, as.numeric(BIRTHEVENT)),
    educationlevel   = as_factor(educationlevel),
    agegroup         = as_factor(agegroup),
    maritalcombined  = as_factor(maritalcombined),
    MCP              = na_if(as.numeric(as.character(MCP)), 98),
    WEALTHQ          = droplevels(as_factor(replace(WEALTHQ, !(as.numeric(WEALTHQ) %in% 1:5), NA))),
    YEAR             = factor(YEAR)
  ) %>%
  as_survey_design(ids = EAID, strata = STRATA, weights = FQWEIGHT, nest = TRUE) %>%
  filter(!is.na(summary_score_av), !is.na(residence),
         !is.na(MCP), !is.na(educationlevel), !is.na(maritalcombined))

#should equal the regression sample (12,150)
nrow(desc_design)
[1] 12150
table1 <- desc_design %>%
  tbl_svysummary(
    by = residence,
    include = c(summary_score_av, BIRTHEVENT, educationlevel, agegroup,
                maritalcombined, MCP, WEALTHQ, YEAR),
    type = list(MCP ~ "dichotomous"),
    value = list(MCP ~ 1),
    statistic = list(
      all_continuous()  ~ "{mean} ({sd})",
      all_categorical() ~ "{p}%"
    ),
    label = list(
      summary_score_av ~ "Pregnancy exercise of choice sub-scale",
      BIRTHEVENT       ~ "Number of births",
      educationlevel   ~ "Educational attainment",
      agegroup         ~ "Age group",
      maritalcombined  ~ "Marital status",
      MCP              ~ "Current modern contraceptive use",
      WEALTHQ          ~ "Wealth quintile",
      YEAR             ~ "Survey year"
    ),
    missing = "no"
  ) %>%
  add_overall() %>%
  add_p() %>%
  modify_header(all_stat_cols() ~ "**{level}**  \nN = {n_unweighted}") %>%
  bold_labels()

table1
Characteristic Overall
N = 121501
Rural
N = 78071
Urban
N = 43431
p-value2
Pregnancy exercise of choice sub-scale 3.95 (0.78) 3.93 (0.80) 4.03 (0.72) 0.14
Number of births 2.91 (2.83) 3.22 (2.99) 2.08 (2.16) <0.001
Educational attainment


<0.001
    none 5.4% 6.4% 2.7%
    primary/middle school 55% 63% 36%
    secondary/post-primary 31% 26% 43%
    tertiary/ post-secondary 8.5% 4.8% 19%
Age group


<0.001
    25-29 17% 16% 21%
    35-39 12% 12% 11%
    15-19 22% 24% 17%
    40-44 8.0% 8.8% 6.0%
    45-49 6.8% 7.2% 5.7%
    20-24 20% 19% 23%
    30-34 14% 14% 15%
Marital status


<0.001
    not in a union 39% 37% 46%
    in a union 61% 63% 54%
Current modern contraceptive use 33% 32% 37% <0.001
Wealth quintile


<0.001
    Lowest quintile 19% 24% 4.4%
    Lower quintile 19% 24% 6.4%
    Middle quintile 20% 23% 11%
    Higher quintile 20% 18% 26%
    Highest quintile 23% 12% 52%
Survey year


0.073
    2020 31% 31% 33%
    2021 35% 37% 28%
    2022 34% 32% 38%
1 Mean (SD); %
2 Design-based KruskalWallis test; Pearson’s X^2: Rao & Scott adjustment

3. Graphics of distribution of outcome by urban status/ wealth

library(srvyr)
library(ggplot2)
library(dplyr)

options(survey.lonely.psu = "adjust")

plotdat <- pmaug2022wgps_sf %>%
  filter(!WEALTHQ %in% c(98, 99)) %>%
  mutate(
    WEALTHQ    = factor(WEALTHQ, levels = 1:5,
                        labels = c("Lowest", "Second", "Middle", "Fourth", "Highest")),
    urbanrural = factor(URBAN, levels = c(0, 1), labels = c("Rural", "Urban"))
  ) %>%
  as_survey_design(ids = EAID, strata = STRATA, weights = FQWEIGHT, nest = TRUE) %>%
  group_by(WEALTHQ, urbanrural) %>%
  summarise(mean_score = survey_mean(summary_score_av, vartype = "ci"), .groups = "drop")

fig <- ggplot(plotdat, aes(x = WEALTHQ, y = mean_score, fill = urbanrural)) +
  geom_col(position = position_dodge(0.8), width = 0.7) +
  geom_errorbar(aes(ymin = mean_score_low, ymax = mean_score_upp),
                position = position_dodge(0.8), width = 0.15, linewidth = 0.4) +
  scale_fill_manual(values = c("Rural" = "#4C6A92", "Urban" = "#C4703F")) +
  coord_cartesian(ylim = c(3, 4.5)) +
  labs(
    x = "Household wealth quintile",
    y = "Mean pregnancy decision-making score",
    fill = "Residence",
    title = "Pregnancy decision-making score by wealth quintile and residence"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    panel.grid.major.x = element_blank(),
    plot.caption = element_text(hjust = 0, color = "grey30"),
    legend.position = "top"
  )

fig

#ggsave("figure_wealth_urban.png", fig, width = 8, height = 5, dpi = 300, bg = "white")

4. Create a new PCA-derived measure separately for those in urban and rural areas; show biplots

#### 4. Wealth index following Rutstein, "Steps to constructing the new DHS Wealth Index" ####
# Replaces Section 4 of "Becca Ch. 3 analysis 9.22.26" (from "#floor/wall/roof materials"
# down to the biplots). Paste it in the same place.
#
#   Step 3   indicators: one 0/1 dummy per category (missing -> 0 on all dummies),
#            yes/no items (missing -> 0), livestock counts continuous (none -> 0)
#   Step 4a  drop zero-variance indicators
#   Step 4b  COMMON PCA on all households (indicators that mean the same thing in urban and rural areas)
#   Step 4c  URBAN PCA on urban households
#   Step 4d  RURAL PCA on rural households
#            All PCAs: correlation matrix, 1 component, mean substitution, unweighted,
#            score = standardized PC1 (same as SPSS regression-method factor score)
#   Step 5   regress COM on URB (urban) and COM on RUR (rural); combined score = a + b * area score
#   Step 6   quintile cutpoints from the distribution weighted by de jure members x household weight
#   Step 7   checks
# Each PMA round (YEAR) is treated as its own survey, as DHS does.

library(dplyr)
library(tidyr)
library(purrr)
library(haven)

#---- settings: check these against your IPUMS PMA codebook ----

hh_weight_var  <- "HQWEIGHT"  # household weight
hh_members_var <- NA          # number of de jure household members (set to its name if in the extract).
                              # If NA, quintiles are weighted by the household weight only (see Step 6).
sleeprooms_var <- NA          # number of rooms used for sleeping (for members per sleeping room); NA = skip
hh_result_var  <- NA          # household questionnaire result; if set, keeps completed interviews (== 1)

# Other Rutstein items, used only if they are in your extract (leave empty if not):
extra_yesno   <- character(0)  # yes/no items, e.g. bank account, toilet shared
extra_categ   <- character(0)  # categorical items, e.g. cooking fuel
extra_contin  <- character(0)  # continuous items, e.g. agricultural land area

# Missing / DK / NIU codes (not given a dummy). If 96 is "Other" in your codebook,
# remove it here so "other" gets its own dummy, as Rutstein specifies.
water_missing  <- c(96, 97, 98, 99)
toilet_missing <- c(95, 96, 97, 98, 99)

#---- household file ----
# Built from pmaug2022 (before the BIRTHEVENT < 90 filter and the female-level
# filters), so the PCA uses every household in the extract, not only households
# of women in the analytic sample. It still includes only households with a
# female record. For all interviewed households, re-extract with household
# members included and set hh_result_var.

hh <- pmaug2022 %>%
  zap_labels() %>%
  distinct(HHID, YEAR, .keep_all = TRUE) %>%
  filter(as.character(URBAN) %in% c("0", "1")) %>%
  mutate(urban = as.integer(as.character(URBAN)))

if (!is.na(hh_result_var)) hh <- filter(hh, .data[[hh_result_var]] == 1)

#---- Step 3: indicator construction ----

# 0/1 dummy for every observed category; households with a missing category get 0 on all
add_dummies <- function(data, cat_var, prefix) {
  levs <- sort(unique(na.omit(data[[cat_var]])))
  for (l in levs) {
    data[[paste0(prefix, "_", l)]] <- as.integer(!is.na(data[[cat_var]]) & data[[cat_var]] == l)
  }
  data
}

# floor/wall/roof: natural materials (1xx) combined into one category, all other codes kept separate
material_cat <- function(x) {
  case_when(
    is.na(x) | x >= 900 ~ NA_character_,
    x %/% 100 == 1      ~ "natural",
    TRUE                ~ as.character(x)
  )
}

hh <- hh %>%
  mutate(
    cat_floor  = material_cat(FLOOR),
    cat_wall   = material_cat(WALLS),
    cat_roof   = material_cat(ROOF),
    # water and toilet: one category per original code (Rutstein 3b.2a/b).
    # If surface-water sources have separate codes, combine them here.
    cat_water  = if_else(WATERDRINKMAIN %in% water_missing, NA_character_, as.character(WATERDRINKMAIN)),
    cat_toilet = if_else(TOILETTYPE %in% toilet_missing, NA_character_, as.character(TOILETTYPE))
  )

cat_specs <- c(cat_floor = "floor", cat_wall = "wall", cat_roof = "roof", cat_water = "water", cat_toilet = "toilet")
for (v in names(cat_specs)) hh <- add_dummies(hh, v, cat_specs[[v]])
for (v in extra_categ) if (v %in% names(hh)) hh <- add_dummies(hh, v, tolower(v))

# yes/no items: 1 = yes, 0 = no / missing / DK / NIU
yesno_raw <- c(electric = "ELECTRC", radio = "RADIO", tv = "TV", mobphone = "MOBPHONE",
               hhphone = "HHPHONE", fridge = "FRIDGE", clock = "CLOCK", cassette = "CASSETTE",
               bed = "BED", cabinet = "CABINET", chair = "CHAIR", sofa = "SOFA", table_own = "TABLE",
               bike = "BIKE", motorcycle = "MOTORCYCL", car = "CAR", drawncart = "DRAWNCART")
extra_yesno <- extra_yesno[extra_yesno %in% names(hh)]
yesno_raw   <- c(yesno_raw[yesno_raw %in% names(hh)], setNames(extra_yesno, tolower(extra_yesno)))

for (nm in names(yesno_raw)) {
  hh[[nm]] <- as.integer(!is.na(hh[[yesno_raw[[nm]]]]) & hh[[yesno_raw[[nm]]]] == 1)
}

# livestock: continuous counts, 0 if the household owns no animals, missing/DK -> NA (mean-substituted later)
livestock_vars <- intersect(c("CHICKENNUM", "SHEEPNUM", "GOATNUM", "PIGNUM", "HORSENUM",
                              "EXOCATTLENUM", "LOCALCATTLENUM", "OTHERLIVESTOCKNUM"), names(hh))

hh <- hh %>%
  mutate(across(all_of(livestock_vars), ~ if_else(.x >= 99990, NA_real_, as.numeric(.x)))) %>%
  mutate(across(all_of(livestock_vars), ~ if_else(!is.na(LIVESTOCKOWN) & LIVESTOCKOWN == 0, 0, .x)))

# other continuous items
extra_contin <- extra_contin[extra_contin %in% names(hh)]

# members per sleeping room (continuous; 0 rooms -> 1; missing -> NA)
if (!is.na(hh_members_var) && !is.na(sleeprooms_var)) {
  hh <- hh %>%
    mutate(rooms = if_else(.data[[sleeprooms_var]] >= 90, NA_real_, as.numeric(.data[[sleeprooms_var]])),
           rooms = if_else(rooms == 0, 1, rooms),
           memsleep = as.numeric(.data[[hh_members_var]]) / rooms)
}

dummy_vars <- grep(paste0("^(", paste(c(unname(cat_specs), tolower(extra_categ)), collapse = "|"), ")_"),
                   names(hh), value = TRUE)
indicator_vars <- c(dummy_vars, names(yesno_raw), livestock_vars, extra_contin,
                    if ("memsleep" %in% names(hh)) "memsleep")

# Step 4b: items left out of the COMMON PCA because they signal different
# levels of wealth in urban and rural areas (Rutstein's example is chickens).
# This list is a judgment call; edit it as needed.
common_exclude <- c(livestock_vars, "drawncart")

#---- Step 4: PCAs ----

# drop columns with no variation
drop_zero_var <- function(data, vars) {
  v <- sapply(data[vars], function(x) var(x, na.rm = TRUE))
  vars[!is.na(v) & v > 0]
}

# unweighted PCA on the correlation matrix, first component, mean substitution
run_pca <- function(data, vars, label, orient_var = "electric") {
  vars <- drop_zero_var(data, vars)
  mat  <- as.matrix(data[vars])
  for (j in seq_len(ncol(mat))) mat[is.na(mat[, j]), j] <- mean(mat[, j], na.rm = TRUE)

  pca   <- prcomp(mat, center = TRUE, scale. = TRUE)
  score <- as.numeric(scale(pca$x[, 1]))  # = regression-method factor score for one component

  # orient so higher = wealthier
  flip <- cor(score, data[[orient_var]]) < 0
  if (isTRUE(flip)) {
    score <- -score
    pca$x[, 1] <- -pca$x[, 1]
    pca$rotation[, 1] <- -pca$rotation[, 1]
  }

  message(label, ": ", length(vars), " indicators, PC1 explains ",
          round(100 * summary(pca)$importance[2, 1], 1), "%")

  list(score = score, loadings = pca$rotation[, 1], vars = vars, pca = pca)
}

# weighted quintiles; cutpoint = last score whose cumulative weighted share is <= 20/40/60/80%
weighted_quintile <- function(score, w) {
  o   <- order(score)
  s   <- score[o]
  cw  <- cumsum(w[o]) / sum(w[o])
  top <- !duplicated(s, fromLast = TRUE)      # cumulative share at each distinct score value
  vals <- s[top]; cum <- cw[top]
  cuts <- sapply(c(.2, .4, .6, .8), function(p) if (any(cum <= p)) max(vals[cum <= p]) else vals[1])
  1L + rowSums(outer(score, cuts, ">"))       # <= cut1 -> 1, (cut1, cut2] -> 2, ...
}

build_wealth_year <- function(d) {
  yr <- unique(d$YEAR)

  # 4a
  vars_all <- drop_zero_var(d, indicator_vars)

  # 4b common
  com <- run_pca(d, setdiff(vars_all, common_exclude), paste(yr, "COMMON"))
  d$com1 <- com$score

  # 4c / 4d urban and rural
  u <- d$urban == 1
  urb <- run_pca(d[u, ],  vars_all, paste(yr, "URBAN"))
  rur <- run_pca(d[!u, ], vars_all, paste(yr, "RURAL"))
  d$urb1 <- NA_real_; d$urb1[u]  <- urb$score
  d$rur1 <- NA_real_; d$rur1[!u] <- rur$score

  # Step 5: regress COM on area score, then combine
  fit_u <- lm(com1 ~ urb1, data = d[u, ])
  fit_r <- lm(com1 ~ rur1, data = d[!u, ])
  d$combscor <- if_else(u, coef(fit_u)[1] + coef(fit_u)[2] * d$urb1,
                           coef(fit_r)[1] + coef(fit_r)[2] * d$rur1)

  step5 <- tibble(YEAR = yr, area = c("urban", "rural"),
                  constant = c(coef(fit_u)[1], coef(fit_r)[1]),
                  coefficient = c(coef(fit_u)[2], coef(fit_r)[2]),
                  r2 = c(summary(fit_u)$r.squared, summary(fit_r)$r.squared),
                  p_coef = c(summary(fit_u)$coefficients[2, 4], summary(fit_r)$coefficients[2, 4]))

  # Step 6: household-member weight
  w_hh <- as.numeric(d[[hh_weight_var]])
  d$hhmemwt <- if (!is.na(hh_members_var)) w_hh * as.numeric(d[[hh_members_var]]) / 1e6 else w_hh
  d$hhmemwt[is.na(d$hhmemwt)] <- 0

  d$ncombsco <- weighted_quintile(d$combscor, d$hhmemwt)
  d$nurb1 <- NA_integer_; d$nurb1[u]  <- weighted_quintile(d$urb1[u],  d$hhmemwt[u])
  d$nrur1 <- NA_integer_; d$nrur1[!u] <- weighted_quintile(d$rur1[!u], d$hhmemwt[!u])

  loadings <- list(com = com$loadings, urb = urb$loadings, rur = rur$loadings) %>%
    imap(~ tibble(indicator = names(.x), !!.y := unname(.x))) %>%
    reduce(full_join, by = "indicator") %>%
    mutate(YEAR = yr, .before = 1) %>%
    arrange(desc(com))

  list(data = d, step5 = step5, loadings = loadings,
       pca = list(com = com$pca, urb = urb$pca, rur = rur$pca))
}

if (is.na(hh_members_var)) {
  message("hh_members_var not set: quintiles weighted by household weight, not de jure members x weight (Rutstein Step 6a).")
}
hh_members_var not set: quintiles weighted by household weight, not de jure members x weight (Rutstein Step 6a).
wealth_by_year <- hh %>%
  group_split(YEAR) %>%
  set_names(map_chr(., ~ as.character(unique(.x$YEAR)))) %>%
  map(build_wealth_year)
2020 COMMON: 64 indicators, PC1 explains 10.7%
2020 URBAN: 68 indicators, PC1 explains 9.6%
2020 RURAL: 72 indicators, PC1 explains 8.4%
2021 COMMON: 64 indicators, PC1 explains 10.4%
2021 URBAN: 68 indicators, PC1 explains 9.5%
2021 RURAL: 71 indicators, PC1 explains 8.5%
2022 COMMON: 66 indicators, PC1 explains 10.3%
2022 URBAN: 72 indicators, PC1 explains 9.3%
2022 RURAL: 69 indicators, PC1 explains 9%
wealth_hh <- map_dfr(wealth_by_year, "data")
step5_regressions <- map_dfr(wealth_by_year, "step5")
loadings_rutstein <- map_dfr(wealth_by_year, "loadings")

step5_regressions   # coefficients should be positive and highly significant
# A tibble: 6 × 6
   YEAR area  constant coefficient    r2 p_coef
  <int> <chr>    <dbl>       <dbl> <dbl>  <dbl>
1  2020 urban    0.590       0.990 0.988      0
2  2020 rural   -0.340       0.823 0.985      0
3  2021 urban    0.593       0.995 0.986      0
4  2021 rural   -0.326       0.825 0.975      0
5  2022 urban    0.575       0.995 0.984      0
6  2022 rural   -0.310       0.840 0.975      0
print(loadings_rutstein, n = Inf)
# A tibble: 221 × 5
     YEAR indicator               com        urb       rur
    <int> <chr>                 <dbl>      <dbl>     <dbl>
  1  2020 floor_340          0.272     0.237      0.288   
  2  2020 tv                 0.267     0.264      0.253   
  3  2020 wall_321           0.262     0.253      0.263   
  4  2020 electric           0.253     0.271      0.239   
  5  2020 roof_242           0.239     0.223      0.252   
  6  2020 sofa               0.212     0.200      0.208   
  7  2020 cabinet            0.208     0.210      0.209   
  8  2020 fridge             0.178     0.190      0.130   
  9  2020 clock              0.159     0.168      0.151   
 10  2020 toilet_6           0.156     0.133      0.148   
 11  2020 car                0.147     0.154      0.132   
 12  2020 bed                0.144     0.120      0.174   
 13  2020 mobphone           0.142     0.142      0.161   
 14  2020 floor_330          0.125     0.124      0.118   
 15  2020 water_2            0.119     0.107      0.0958  
 16  2020 water_3            0.119     0.103      0.0689  
 17  2020 toilet_7           0.117     0.0325     0.158   
 18  2020 radio              0.116     0.0916     0.157   
 19  2020 water_1            0.114     0.129      0.0538  
 20  2020 table_own          0.106     0.0761     0.152   
 21  2020 toilet_2           0.102     0.103      0.0781  
 22  2020 toilet_1           0.0865    0.100      0.0305  
 23  2020 motorcycle         0.0819    0.0573     0.139   
 24  2020 wall_310           0.0716    0.0503     0.0867  
 25  2020 cassette           0.0560    0.0677     0.0499  
 26  2020 water_13           0.0477    0.0476     0.0468  
 27  2020 hhphone            0.0459    0.0446     0.0540  
 28  2020 chair              0.0442    0.0383     0.102   
 29  2020 roof_340           0.0428    0.0515     0.0487  
 30  2020 toilet_3           0.0389    0.0370     0.0231  
 31  2020 roof_351           0.0348    0.0293     0.0473  
 32  2020 water_9            0.0277    0.0272     0.0469  
 33  2020 toilet_5           0.0181    0.0173     0.0100  
 34  2020 roof_320           0.0159    0.00857    0.0241  
 35  2020 water_10           0.0157    0.0225     0.00764 
 36  2020 toilet_4           0.0143    0.0118    NA       
 37  2020 roof_400           0.0103   NA          0.0337  
 38  2020 water_11           0.0102   NA          0.0240  
 39  2020 wall_350           0.00969  NA          0.0183  
 40  2020 wall_400           0.00779  -0.00796    0.00733 
 41  2020 floor_380          0.00750   0.00192    0.00857 
 42  2020 wall_233           0.00647  NA          0.0153  
 43  2020 floor_390          0.00565  -0.0000393  0.0169  
 44  2020 floor_400          0.00530  -0.0128     0.0205  
 45  2020 water_5            0.00510  -0.00434    0.0127  
 46  2020 water_7            0.000945 -0.0436    -0.00277 
 47  2020 floor_310          0.000916 -0.0135     0.00927 
 48  2020 toilet_9          -0.000361  0.00306    0.0110  
 49  2020 toilet_12         -0.00246  -0.0104     0.00416 
 50  2020 roof_230          -0.00516  -0.0206     0.00633 
 51  2020 wall_281          -0.00551  -0.0118     0.00221 
 52  2020 toilet_11         -0.00712  -0.00644    0.000145
 53  2020 water_6           -0.0197   -0.0417     0.0114  
 54  2020 water_8           -0.0245   -0.0443    -0.0112  
 55  2020 bike              -0.0245   -0.0469     0.0424  
 56  2020 water_12          -0.0563   -0.0222    -0.0230  
 57  2020 wall_322          -0.0611   -0.0561    -0.0383  
 58  2020 toilet_20         -0.131    -0.160     -0.143   
 59  2020 water_4           -0.146    -0.198     -0.0795  
 60  2020 wall_280          -0.151    -0.177     -0.155   
 61  2020 wall_natural      -0.163    -0.197     -0.140   
 62  2020 toilet_8          -0.186    -0.181     -0.130   
 63  2020 roof_natural      -0.253    -0.246     -0.267   
 64  2020 floor_natural     -0.310    -0.307     -0.310   
 65  2020 drawncart         NA        -0.0417    -0.0149  
 66  2020 CHICKENNUM        NA         0.0407     0.0561  
 67  2020 SHEEPNUM          NA         0.00883    0.00896 
 68  2020 GOATNUM           NA         0.0494     0.0399  
 69  2020 PIGNUM            NA         0.0240     0.0707  
 70  2020 EXOCATTLENUM      NA         0.0732     0.0680  
 71  2020 LOCALCATTLENUM    NA         0.0432     0.0253  
 72  2020 OTHERLIVESTOCKNUM NA        -0.00717    0.00700 
 73  2020 HORSENUM          NA        NA         -0.0108  
 74  2021 tv                 0.283     0.287      0.260   
 75  2021 floor_340          0.270     0.221      0.290   
 76  2021 electric           0.242     0.271      0.226   
 77  2021 sofa               0.242     0.227      0.242   
 78  2021 roof_242           0.239     0.220      0.253   
 79  2021 wall_321           0.231     0.216      0.232   
 80  2021 cabinet            0.212     0.210      0.226   
 81  2021 fridge             0.177     0.185      0.120   
 82  2021 clock              0.167     0.169      0.158   
 83  2021 toilet_6           0.161     0.130      0.170   
 84  2021 bed                0.153     0.155      0.179   
 85  2021 car                0.142     0.147      0.122   
 86  2021 floor_330          0.138     0.138      0.109   
 87  2021 mobphone           0.134     0.149      0.138   
 88  2021 water_2            0.126     0.120      0.0855  
 89  2021 radio              0.123     0.126      0.165   
 90  2021 water_1            0.120     0.124      0.0592  
 91  2021 toilet_2           0.106     0.109      0.0547  
 92  2021 table_own          0.105     0.0800     0.156   
 93  2021 toilet_7           0.0828   -0.000101   0.117   
 94  2021 motorcycle         0.0802    0.0468     0.146   
 95  2021 wall_310           0.0799    0.0653     0.0673  
 96  2021 cassette           0.0791    0.0726     0.0911  
 97  2021 toilet_1           0.0779    0.0733     0.0492  
 98  2021 water_3            0.0774    0.0743     0.0164  
 99  2021 water_9            0.0429    0.0377     0.0667  
100  2021 roof_340           0.0412    0.0275     0.0679  
101  2021 toilet_3           0.0379    0.0373     0.0321  
102  2021 hhphone            0.0368    0.0426     0.0199  
103  2021 wall_281           0.0361    0.0132     0.0422  
104  2021 wall_400           0.0336    0.00639    0.0579  
105  2021 water_13           0.0322    0.0305     0.0103  
106  2021 wall_350           0.0307    0.0319     0.0303  
107  2021 chair              0.0272    0.0350     0.0981  
108  2021 roof_351           0.0257    0.0116     0.00443 
109  2021 toilet_4           0.0254    0.0200    NA       
110  2021 toilet_5           0.0165    0.0292    -0.00369 
111  2021 roof_320           0.0110    0.0217     0.00275 
112  2021 floor_390          0.00746  -0.00729    0.0185  
113  2021 water_11           0.00647   0.00127    0.00654 
114  2021 water_14           0.00387  -0.000768  NA       
115  2021 water_5            0.00375  -0.0171     0.00524 
116  2021 water_10           0.00372  NA          0.00674 
117  2021 floor_380          0.00336  -0.00784    0.00890 
118  2021 floor_400          0.00290  -0.0114     0.0154  
119  2021 floor_310         -0.00168  NA          0.000796
120  2021 toilet_9          -0.00448  -0.0425     0.0169  
121  2021 water_7           -0.00650  -0.0332    -0.00812 
122  2021 toilet_12         -0.0102   NA         -0.00912 
123  2021 roof_230          -0.0140   NA         -0.0136  
124  2021 roof_400          -0.0155   -0.0339    -0.0106  
125  2021 water_8           -0.0202   -0.0396     0.00427 
126  2021 water_6           -0.0223   -0.0355     0.0124  
127  2021 bike              -0.0284   -0.0381     0.0507  
128  2021 toilet_11         -0.0293   -0.0889    -0.000562
129  2021 water_12          -0.0414   -0.0439    -0.0208  
130  2021 wall_322          -0.0442   -0.0647    -0.0144  
131  2021 toilet_22         -0.107    -0.0844    -0.126   
132  2021 water_4           -0.140    -0.196     -0.0593  
133  2021 wall_280          -0.156    -0.177     -0.150   
134  2021 wall_natural      -0.177    -0.203     -0.155   
135  2021 toilet_8          -0.183    -0.166     -0.134   
136  2021 roof_natural      -0.252    -0.248     -0.260   
137  2021 floor_natural     -0.316    -0.314     -0.310   
138  2021 drawncart         NA        -0.0407    -0.0439  
139  2021 CHICKENNUM        NA         0.0439     0.0519  
140  2021 SHEEPNUM          NA        -0.000818   0.0100  
141  2021 GOATNUM           NA         0.0403     0.0644  
142  2021 PIGNUM            NA         0.0340     0.0954  
143  2021 EXOCATTLENUM      NA         0.0515     0.0689  
144  2021 LOCALCATTLENUM    NA         0.0371     0.0620  
145  2021 OTHERLIVESTOCKNUM NA         0.0205     0.0302  
146  2021 HORSENUM          NA        NA         -0.0271  
147  2022 tv                 0.277     0.274      0.259   
148  2022 electric           0.249     0.279      0.226   
149  2022 roof_242           0.247     0.233      0.260   
150  2022 sofa               0.239     0.210      0.240   
151  2022 floor_340          0.233     0.165      0.267   
152  2022 wall_321           0.222     0.180      0.248   
153  2022 cabinet            0.222     0.212      0.228   
154  2022 fridge             0.183     0.188      0.134   
155  2022 clock              0.164     0.165      0.147   
156  2022 bed                0.154     0.152      0.177   
157  2022 toilet_6           0.150     0.121      0.149   
158  2022 floor_330          0.146     0.152      0.116   
159  2022 mobphone           0.145     0.185      0.136   
160  2022 radio              0.145     0.148      0.169   
161  2022 car                0.131     0.141      0.114   
162  2022 water_1            0.121     0.131      0.0373  
163  2022 water_2            0.112     0.109      0.0733  
164  2022 table_own          0.103     0.0847     0.154   
165  2022 toilet_2           0.102     0.0977     0.0798  
166  2022 motorcycle         0.0997    0.0671     0.149   
167  2022 toilet_1           0.0940    0.102     NA       
168  2022 toilet_7           0.0914    0.0167     0.126   
169  2022 cassette           0.0827    0.0831     0.0947  
170  2022 wall_310           0.0824    0.0650     0.0696  
171  2022 floor_320          0.0654    0.0350     0.0585  
172  2022 water_3            0.0634    0.0698     0.0142  
173  2022 water_9            0.0540    0.0441     0.0797  
174  2022 wall_281           0.0522    0.0384     0.0450  
175  2022 hhphone            0.0406    0.0335     0.0487  
176  2022 wall_350           0.0378    0.0504     0.0300  
177  2022 toilet_3           0.0348    0.0187     0.0458  
178  2022 chair              0.0324    0.0354     0.0970  
179  2022 water_13           0.0312    0.0239     0.00824 
180  2022 water_16           0.0297    0.0112     0.00343 
181  2022 water_15           0.0277    0.0243     0.0157  
182  2022 roof_320           0.0276    0.0309     0.0125  
183  2022 roof_400           0.0246    0.0215    NA       
184  2022 wall_400           0.0232    0.00735    0.0254  
185  2022 roof_340           0.0231    0.0226    NA       
186  2022 floor_400          0.0213    0.0246    NA       
187  2022 water_5            0.0206   -0.00576    0.0264  
188  2022 toilet_5           0.0166    0.0139    NA       
189  2022 toilet_4           0.0123    0.0135    NA       
190  2022 water_17           0.00893   0.000792   0.0141  
191  2022 water_14           0.00514   0.0000833  0.00651 
192  2022 toilet_9           0.00504   0.00799    0.0141  
193  2022 floor_380          0.00404  -0.00233    0.00702 
194  2022 water_10           0.000650 -0.0124     0.0153  
195  2022 water_7            0.000167 -0.00365    0.00478 
196  2022 floor_390         -0.000551 -0.0113     0.00822 
197  2022 roof_243          -0.0112   NA         -0.0116  
198  2022 toilet_11         -0.0117   NA         -0.00779 
199  2022 floor_220         -0.0132   NA         -0.0137  
200  2022 toilet_12         -0.0163   -0.0189    -0.0149  
201  2022 bike              -0.0179   -0.0529     0.0453  
202  2022 water_8           -0.0194   -0.0202    -0.0117  
203  2022 water_6           -0.0299   -0.0592     0.00817 
204  2022 water_12          -0.0383   -0.0396    -0.0151  
205  2022 wall_322          -0.0436   -0.0636    -0.0200  
206  2022 toilet_22         -0.113    -0.103     -0.129   
207  2022 water_4           -0.148    -0.224     -0.0672  
208  2022 wall_natural      -0.155    -0.192     -0.132   
209  2022 wall_280          -0.178    -0.161     -0.181   
210  2022 toilet_8          -0.183    -0.187     -0.129   
211  2022 roof_natural      -0.253    -0.254     -0.260   
212  2022 floor_natural     -0.307    -0.302     -0.300   
213  2022 drawncart         NA        -0.131     -0.0655  
214  2022 CHICKENNUM        NA         0.0276     0.0582  
215  2022 SHEEPNUM          NA         0.0349     0.0469  
216  2022 GOATNUM           NA         0.0564     0.0868  
217  2022 PIGNUM            NA         0.0240     0.0815  
218  2022 HORSENUM          NA        -0.0111    -0.0302  
219  2022 EXOCATTLENUM      NA         0.0437     0.0829  
220  2022 LOCALCATTLENUM    NA         0.0372     0.0582  
221  2022 OTHERLIVESTOCKNUM NA         0.0127     0.0545  
#---- Step 7: checks ----

# 7a: each quintile should hold ~20% of the population (weighted by hhmemwt)
wealth_hh %>%
  group_by(YEAR, ncombsco) %>%
  summarise(w = sum(hhmemwt), .groups = "drop_last") %>%
  mutate(pct = round(100 * w / sum(w), 1)) %>%
  select(-w) %>%
  pivot_wider(names_from = ncombsco, values_from = pct)
# A tibble: 3 × 6
# Groups:   YEAR [3]
   YEAR   `1`   `2`   `3`   `4`   `5`
  <int> <dbl> <dbl> <dbl> <dbl> <dbl>
1  2020    20    20    20    20    20
2  2021    20    20    20    20    20
3  2022    20    20    20    20    20
# 7b: indicators by quintile (household-weighted %), should rise or fall across quintiles as expected
step7_indicators <- wealth_hh %>%
  mutate(w = as.numeric(.data[[hh_weight_var]])) %>%
  group_by(YEAR, ncombsco) %>%
  summarise(across(all_of(c(dummy_vars, names(yesno_raw))), ~ 100 * weighted.mean(.x, w, na.rm = TRUE)),
            .groups = "drop") %>%
  pivot_longer(-c(YEAR, ncombsco), names_to = "indicator", values_to = "pct") %>%
  pivot_wider(names_from = ncombsco, names_prefix = "Q", values_from = pct)

print(filter(step7_indicators, indicator %in% c("electric", "tv", "fridge", "floor_natural")), n = Inf)
# A tibble: 12 × 7
    YEAR indicator          Q1      Q2     Q3    Q4     Q5
   <int> <chr>           <dbl>   <dbl>  <dbl> <dbl>  <dbl>
 1  2020 floor_natural 99.9    99.7    78.2   15.8   0.738
 2  2020 electric       1.44   18.0    41.7   64.0  94.0  
 3  2020 tv             0.197   0.586   4.06  22.6  83.4  
 4  2020 fridge         0       0       0.363  1.12 26.4  
 5  2021 floor_natural 99.8    99.1    83.9   18.1   0.926
 6  2021 electric       7.65   29.4    50.4   64.6  94.7  
 7  2021 tv             0       0.698   5.77  25.2  88.0  
 8  2021 fridge         0       0.0818  1.09   1.05 25.8  
 9  2022 floor_natural 99.6    95.1    47.2    5.27  0.426
10  2022 electric       7.10   29.8    47.6   84.2  96.2  
11  2022 tv             0.0628  1.66   10.6   54.7  93.5  
12  2022 fridge         0       0       0.605  3.59 40.7  
# urban/rural share by combined quintile (urban should concentrate in the top quintiles)
table(wealth_hh$YEAR, wealth_hh$ncombsco, wealth_hh$urban)
, ,  = 0

      
         1   2   3   4   5
  2020 493 495 444 307 156
  2021 547 499 467 351 206
  2022 622 600 454 250 146

, ,  = 1

      
         1   2   3   4   5
  2020 103  79 160 307 444
  2021 113  89 119 280 537
  2022 130 106 169 335 377
#---- merge back ----

q_labels <- c("Lowest", "Second", "Middle", "Fourth", "Highest")

wealth_rutstein <- wealth_hh %>%
  transmute(HHID, YEAR, com1, urb1, rur1, combscor,
            ncombsco = factor(ncombsco, levels = 1:5, labels = q_labels),
            nurb1    = factor(nurb1,    levels = 1:5, labels = q_labels),
            nrur1    = factor(nrur1,    levels = 1:5, labels = q_labels))

pmaug2022wgps <- pmaug2022wgps %>%
  left_join(wealth_rutstein, by = c("HHID", "YEAR"))

# ---- biplots (one per area and year) ----
library(factoextra)
Welcome to factoextra!
Want to learn more? See two factoextra-related books at https://www.datanovia.com/library/principal-component-methods
for (yr in names(wealth_by_year)) {
  for (a in c("urb", "rur")) {
    print(fviz_pca_biplot(
      wealth_by_year[[yr]]$pca[[a]],
      select.var = list(contrib = 15),
      geom.ind = "none",
      label = "var",
      col.var = "steelblue",
      repel = TRUE,
      title = paste0("PCA biplot: ", if (a == "urb") "Urban" else "Rural", " household wealth indicators, ", yr)
    ))
  }
}

5. Map out cluster level pregnancy decision-making, specifying which clusters are urban

#map out the scale by EAID

UG<-st_transform(UG_boundary_4, crs=4326)

UG <- st_make_valid(UG)

joined<-st_join(UG, pmaug2022wgps_sf, join=st_intersects)

# Set the global option
options(survey.lonely.psu = "adjust")

library(dplyr)


joined<-joined%>%
  filter(summary_score_av!='NA')

joined$fertautonomyscale <- as.numeric(joined$summary_score_av)

library(dplyr)
library(srvyr)

pmaug2022groupedweighted2 <- joined  %>%
  as_survey_design(ids=EAID, strata = STRATA, weights = FQWEIGHT, nest=TRUE) %>%
  group_by(EAID, YEAR, URBAN) %>%
  summarise(
    summary_score_av_mean = survey_mean(summary_score_av, na.rm = TRUE)
  )



#make map of cluster level pregnancy decision-making

UG_PMA_GPS_2022 <- UG_PMA_GPS_2022 %>%
  mutate(EAID = as.numeric(EAID))

mapdatareg<-left_join(pmaug2022groupedweighted2, UG_PMA_GPS_2022)
Joining with `by = join_by(EAID)`
#convert to a spatial frame

mapdatareg<- st_as_sf(mapdatareg, coords = c("GPSLONG", "GPSLAT"), crs=4326)


mapdatareg <- mapdatareg %>%
  mutate(residence = factor(URBAN, levels = c(0, 1), labels = c("Rural", "Urban")))

#all on one map

ggplot(data = mapdatareg) +
  geom_sf(data = UG_boundary_4, fill = NA, color = "lightgray", linewidth = 0.2, inherit.aes = FALSE) +
  geom_sf(data = UG_sub_boundary, fill = NA, color = "lightgray", linewidth = 0.2, inherit.aes = FALSE) +
  geom_sf(aes(color = summary_score_av_mean, shape = residence), size = 1.6, alpha = 0.8) +
  scale_color_viridis_c(option = "magma", direction = -1, name = "Pregnancy exercise of choice\nsub-scale mean") +
  scale_shape_manual(values = c(Rural = 15, Urban = 17), name = NULL) +
  facet_wrap(~ YEAR) +
  theme_minimal(base_size = 12) +
  theme(
    legend.title = element_text(size = 10),
    legend.text  = element_text(size = 9),
    strip.text   = element_text(size = 12, face = "bold"),
    axis.text    = element_blank(),
    panel.grid   = element_blank()
  )
Warning: Removed 243 rows containing missing values or values outside the scale range
(`geom_sf()`).

#separated by panel

mapdatareg %>%
  filter(!is.na(residence)) %>%
  ggplot() +
  geom_sf(data = UG_boundary_4, fill = NA, color = "lightgray", linewidth = 0.2, inherit.aes = FALSE) +
  geom_sf(data = UG_sub_boundary, fill = NA, color = "lightgray", linewidth = 0.2, inherit.aes = FALSE) +
  geom_sf(aes(color = summary_score_av_mean), size = 1.8, alpha = 0.8) +
  scale_color_viridis_c(option = "magma", direction = -1, name = "Pregnancy exercise of choice\nsub-scale mean") +
  facet_grid(residence ~ YEAR) +
  theme_minimal(base_size = 12) +
  theme(
    legend.title = element_text(size = 10),
    legend.text  = element_text(size = 9),
    strip.text   = element_text(size = 12, face = "bold"),
    axis.text    = element_blank(),
    panel.grid   = element_blank()
  )

6. Map out original and udpated measures of wealth

wealth_clusters <- pmaug2022wgps %>%
  as_survey_design(ids = EAID, strata = STRATA, weights = FQWEIGHT, nest = TRUE) %>%
  group_by(EAID, YEAR) %>%
  summarise(
    wealth_orig_mean = survey_mean(SCORE,    na.rm = TRUE),
    wealth_pca_mean  = survey_mean(combscor, na.rm = TRUE)
  ) %>%
  left_join(UG_PMA_GPS_2022, by = "EAID") %>%
  st_as_sf(coords = c("GPSLONG", "GPSLAT"), crs = 4326)


map_wealth <- function(var, legend_title) {
  ggplot(wealth_clusters) +
    geom_sf(data = UG_boundary_4, fill = NA, color = "lightgray", linewidth = 0.2, inherit.aes = FALSE) +
    geom_sf(data = UG_sub_boundary, fill = NA, color = "lightgray", linewidth = 0.2, inherit.aes = FALSE) +
    geom_sf(aes(color = .data[[var]]), size = 1.8, alpha = 0.8) +
    scale_color_gradient2(low = "#b2182b", mid = "grey85", high = "#2166ac",
                          midpoint = 0, name = legend_title) +
    facet_wrap(~ YEAR) +
    theme_minimal(base_size = 12) +
    theme(
      legend.title = element_text(size = 10),
      legend.text  = element_text(size = 9),
      strip.text   = element_text(size = 12, face = "bold"),
      axis.text    = element_blank(),
      panel.grid   = element_blank()
    )
}

p_wealth_orig <- map_wealth("wealth_orig_mean", "Original wealth score\ncluster mean")
p_wealth_pca  <- map_wealth("wealth_pca_mean",  "PCA-derived wealth score\ncluster mean")

p_wealth_orig

p_wealth_pca

7. Regression analysis

#drop missing values



individualandcluster<-left_join(joined, pmaug2022groupedweighted2)
Joining with `by = join_by(EAID, YEAR, URBAN)`
#drop missing values

individualandcluster<-individualandcluster%>%
  filter(MCP!='98')


individualandcluster<-individualandcluster%>%
  filter(educationlevel!='NA')



individualandcluster<-individualandcluster%>%
  filter(maritalcombined!='NA')



individualandcluster<-individualandcluster%>%
  filter(URBAN!='98')
library(modelsummary)



library(lme4)
Loading required package: Matrix

Attaching package: 'Matrix'
The following objects are masked from 'package:tidyr':

    expand, pack, unpack
Registered S3 method overwritten by 'lme4':
  method           from
  na.action.merMod car 
# relationship with urban status and summary score: controlling for year and a random effect for individual 

modelbiv <- lmer(summary_score_av ~factor(URBAN)+ factor(YEAR)+ (1|FQINSTID), 
              data = individualandcluster)

modelsummary(
  modelbiv,
  slope = "b",
  stars = TRUE,
  title = "Bivariate relationship between urban/ rural status and pregnancy decision-makiong",
  gof_map = list(list(raw = "nobs", clean = "N", fmt = 0))
) 
Bivariate relationship between urban/ rural status and pregnancy decision-makiong
(1)
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
(Intercept) 3.879***
(0.014)
factor(URBAN)1 0.070***
(0.017)
factor(YEAR)2021 0.061***
(0.016)
factor(YEAR)2022 0.015
(0.016)
SD (Intercept FQINSTID) 0.436
SD (Observations) 0.659
N 12150
#add a random effect for cluster


modelbivwithcluster <- lmer(summary_score_av ~ factor(URBAN)+ factor(YEAR)+ (1|EAID)+ (1|FQINSTID), 
              data = individualandcluster)

modelsummary(
  modelbivwithcluster,
  slope = "b",
  stars = TRUE,
  title = "Relationship between urban/ rural status and pregnancy decision-making",
  gof_map = list(list(raw = "nobs", clean = "N", fmt = 0))
) 
Relationship between urban/ rural status and pregnancy decision-making
(1)
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
(Intercept) 3.896***
(0.038)
factor(URBAN)1 0.089
(0.060)
factor(YEAR)2021 0.040**
(0.015)
factor(YEAR)2022 -0.003
(0.016)
SD (Intercept FQINSTID) 0.287
SD (Intercept EAID) 0.331
SD (Observations) 0.658
N 12150

Note: adding a random effect for cluster renders urban as insignificant when controlling for year in addition to a random effect for individual.

However, if we build a model that only includes a random effect for individual, not for cluster, urban has a significant and negative on pregnancy decision-making, so the suppression by wealth is happening here.

#full model without the cluster random effect 

library(lme4)

modelmulti <- lmer(summary_score_av ~BIRTHEVENT + educationlevel + agegroup+ maritalcombined+ factor(MCP)+ factor(URBAN)+ factor(WEALTHQ)+ factor(YEAR)+ (1|FQINSTID), 
              data = individualandcluster)

modelsummary(
  modelmulti,
  slope = "b",
  stars = TRUE,
  title = "Relationship between urban/ rural status and pregnancy decision-making, individual random effect only",
  gof_map = list(list(raw = "nobs", clean = "N", fmt = 0))
) 
Relationship between urban/ rural status and pregnancy decision-making, individual random effect only
(1)
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
(Intercept) 3.708***
(0.038)
BIRTHEVENT -0.014**
(0.005)
educationlevelprimary/middle school -0.007
(0.033)
educationlevelsecondary/post-primary 0.210***
(0.036)
educationleveltertiary/ post-secondary 0.263***
(0.044)
agegroup20-24 -0.017
(0.024)
agegroup25-29 0.005
(0.028)
agegroup30-34 0.047
(0.033)
agegroup35-39 -0.001
(0.038)
agegroup40-44 -0.032
(0.043)
agegroup45-49 -0.029
(0.046)
maritalcombinedin a union -0.052**
(0.018)
factor(MCP)1 0.105***
(0.016)
factor(URBAN)1 -0.096***
(0.018)
factor(WEALTHQ)2 0.136***
(0.023)
factor(WEALTHQ)3 0.222***
(0.024)
factor(WEALTHQ)4 0.242***
(0.026)
factor(WEALTHQ)5 0.299***
(0.028)
factor(YEAR)2021 0.056***
(0.016)
factor(YEAR)2022 0.026
(0.016)
SD (Intercept FQINSTID) 0.392
SD (Observations) 0.658
N 12150
#full model with the cluster random effect


modelmulti_EAID <- lmer(summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined +
                          factor(MCP) + factor(URBAN) + factor(WEALTHQ) + factor(YEAR) +
                          (1 | FQINSTID) + (1 | EAID),
                        data = individualandcluster)

modelsummary(
  modelmulti_EAID,
  stars = TRUE,
  title = "Relationship between urban/rural status and pregnancy decision-making (IPUMS wealth) with random effect for EAID",
 gof_map = list(list(raw = "nobs", clean = "N", fmt = 0))
)
Relationship between urban/rural status and pregnancy decision-making (IPUMS wealth) with random effect for EAID
(1)
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
(Intercept) 3.733***
(0.051)
BIRTHEVENT -0.018***
(0.005)
educationlevelprimary/middle school 0.078*
(0.033)
educationlevelsecondary/post-primary 0.269***
(0.036)
educationleveltertiary/ post-secondary 0.338***
(0.043)
agegroup20-24 -0.016
(0.022)
agegroup25-29 0.011
(0.026)
agegroup30-34 0.057+
(0.030)
agegroup35-39 0.026
(0.034)
agegroup40-44 -0.010
(0.039)
agegroup45-49 -0.004
(0.042)
maritalcombinedin a union -0.037*
(0.017)
factor(MCP)1 0.109***
(0.015)
factor(URBAN)1 -0.042
(0.056)
factor(WEALTHQ)2 0.051*
(0.026)
factor(WEALTHQ)3 0.098***
(0.027)
factor(WEALTHQ)4 0.114***
(0.029)
factor(WEALTHQ)5 0.145***
(0.032)
factor(YEAR)2021 0.037*
(0.015)
factor(YEAR)2022 0.004
(0.016)
SD (Intercept FQINSTID) 0.254
SD (Intercept EAID) 0.304
SD (Observations) 0.658
N 12150

Now if we run this with the PCA-derived measure of wealth, urban is not significant.

##run this with the Rutstein combined wealth index

individualandcluster <- individualandcluster %>%
  left_join(wealth_rutstein %>% select(HHID, YEAR, combscor, ncombsco),
            by = c("HHID", "YEAR"))

modelmulti_pca <- lmer(summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined +
                         factor(MCP) + factor(URBAN) + ncombsco + factor(YEAR) +
                         (1 | FQINSTID),
                       data = individualandcluster)


modelsummary(
  modelmulti_pca,
  stars = TRUE,
  title = "Relationship between urban/rural status and pregnancy decision-making (PCA-derived wealth)",
 gof_map = list(list(raw = "nobs", clean = "N", fmt = 0))
)
Relationship between urban/rural status and pregnancy decision-making (PCA-derived wealth)
(1)
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
(Intercept) 3.699***
(0.038)
BIRTHEVENT -0.015**
(0.005)
educationlevelprimary/middle school -0.010
(0.033)
educationlevelsecondary/post-primary 0.210***
(0.036)
educationleveltertiary/ post-secondary 0.262***
(0.044)
agegroup20-24 -0.013
(0.024)
agegroup25-29 0.008
(0.028)
agegroup30-34 0.051
(0.033)
agegroup35-39 0.003
(0.038)
agegroup40-44 -0.028
(0.043)
agegroup45-49 -0.028
(0.046)
maritalcombinedin a union -0.053**
(0.018)
factor(MCP)1 0.105***
(0.016)
factor(URBAN)1 -0.088***
(0.019)
ncombscoSecond 0.161***
(0.023)
ncombscoMiddle 0.237***
(0.024)
ncombscoFourth 0.234***
(0.025)
ncombscoHighest 0.299***
(0.028)
factor(YEAR)2021 0.056***
(0.016)
factor(YEAR)2022 0.028+
(0.016)
SD (Intercept FQINSTID) 0.393
SD (Observations) 0.658
N 12150

Now when we add a random effect for cluster, urban remains non-significant.

modelmulti_pca_EAID <- lmer(summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined +
                              factor(MCP) + factor(URBAN) + ncombsco + factor(YEAR) +
                              (1 | FQINSTID) + (1 | EAID),
                            data = individualandcluster)

modelsummary(
  modelmulti_pca_EAID,
  stars = TRUE,
  title = "Relationship between urban/rural status and pregnancy decision-making (PCA-derived wealth) with random effect for EAID",
   gof_map = list(list(raw = "nobs", clean = "N", fmt = 0))
)
Relationship between urban/rural status and pregnancy decision-making (PCA-derived wealth) with random effect for EAID
(1)
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
(Intercept) 3.717***
(0.051)
BIRTHEVENT -0.019***
(0.005)
educationlevelprimary/middle school 0.078*
(0.033)
educationlevelsecondary/post-primary 0.270***
(0.036)
educationleveltertiary/ post-secondary 0.339***
(0.043)
agegroup20-24 -0.015
(0.022)
agegroup25-29 0.011
(0.026)
agegroup30-34 0.057+
(0.030)
agegroup35-39 0.027
(0.034)
agegroup40-44 -0.008
(0.039)
agegroup45-49 -0.004
(0.042)
maritalcombinedin a union -0.037*
(0.017)
factor(MCP)1 0.109***
(0.015)
factor(URBAN)1 -0.036
(0.056)
ncombscoSecond 0.088***
(0.024)
ncombscoMiddle 0.120***
(0.026)
ncombscoFourth 0.113***
(0.028)
ncombscoHighest 0.153***
(0.031)
factor(YEAR)2021 0.037*
(0.015)
factor(YEAR)2022 0.005
(0.016)
SD (Intercept FQINSTID) 0.255
SD (Intercept EAID) 0.303
SD (Observations) 0.658
N 12150

7. How much between-EA variance is explained urban status or wealth?

library(lme4)
library(performance)
library(dplyr)


model_null <- lmer(
  summary_score_av ~ (1|FQINSTID) + (1|EAID),
  data = individualandcluster
)

icc(model_null, by_group = TRUE)
# ICC by Group

Group    |   ICC
----------------
FQINSTID | 0.131
EAID     | 0.177
model_urban_only <- lmer(
  summary_score_av ~ factor(URBAN) + (1|FQINSTID) + (1|EAID),
  data = individualandcluster
)

icc(model_urban_only, by_group = TRUE)
# ICC by Group

Group    |   ICC
----------------
FQINSTID | 0.132
EAID     | 0.176
# ---- Extract variance components from both models ----
var_null  <- as.data.frame(VarCorr(model_null))
var_urban <- as.data.frame(VarCorr(model_urban_only))

eaid_var_null   <- var_null$vcov[var_null$grp == "EAID"]
eaid_var_urban  <- var_urban$vcov[var_urban$grp == "EAID"]

fqinstid_var_null  <- var_null$vcov[var_null$grp == "FQINSTID"]
fqinstid_var_urban <- var_urban$vcov[var_urban$grp == "FQINSTID"]

residual_var_null  <- var_null$vcov[var_null$grp == "Residual"]
residual_var_urban <- var_urban$vcov[var_urban$grp == "Residual"]

# ---- Build a before/after comparison table ----
variance_decomp_table <- tibble(
  component = c("EAID (between-cluster)", "FQINSTID (within-person)", "Residual"),
  variance_before_urban = c(eaid_var_null, fqinstid_var_null, residual_var_null),
  variance_after_urban  = c(eaid_var_urban, fqinstid_var_urban, residual_var_urban)
) %>%
  mutate(
    pct_change = 100 * (variance_before_urban - variance_after_urban) / variance_before_urban
  )

print(variance_decomp_table)
# A tibble: 3 × 4
  component                variance_before_urban variance_after_urban pct_change
  <chr>                                    <dbl>                <dbl>      <dbl>
1 EAID (between-cluster)                  0.111                0.110     0.785  
2 FQINSTID (within-person)                0.0823               0.0823    0.0287 
3 Residual                                0.434                0.434    -0.00285
pct_eaid_explained_by_urban <- 100 * (eaid_var_null - eaid_var_urban) / eaid_var_null

cat(sprintf(
  "Between-EA variance before adding URBAN: %.4f\nBetween-EA variance after adding URBAN:  %.4f\nPercent of between-EA variance explained by urban/rural status: %.1f%%\n",
  eaid_var_null, eaid_var_urban, pct_eaid_explained_by_urban
))
Between-EA variance before adding URBAN: 0.1108
Between-EA variance after adding URBAN:  0.1099
Percent of between-EA variance explained by urban/rural status: 0.8%
reg_vars <- c("summary_score_av", "BIRTHEVENT", "educationlevel", "agegroup",
              "maritalcombined", "MCP", "URBAN", "WEALTHQ",
              "ncombsco", "YEAR", "FQINSTID", "EAID")
icc_data <- individualandcluster %>%
  filter(if_all(all_of(reg_vars), ~ !is.na(.x)))

get_eaid_var <- function(model) {
  vc <- as.data.frame(VarCorr(model))
  vc$vcov[vc$grp == "EAID"]
}

model_null_common  <- lmer(summary_score_av ~ 1 + (1|FQINSTID) + (1|EAID), data = icc_data)
model_wealth_ipums <- lmer(summary_score_av ~ factor(WEALTHQ) + (1|FQINSTID) + (1|EAID), data = icc_data)
model_wealth_pca   <- lmer(summary_score_av ~ ncombsco + (1|FQINSTID) + (1|EAID), data = icc_data)
eaid_var_null_common <- get_eaid_var(model_null_common)

wealth_var_compare <- tibble(
  model    = c("Null", "IPUMS wealth quintile", "Stratified PCA wealth quintile"),
  eaid_var = c(eaid_var_null_common,
               get_eaid_var(model_wealth_ipums),
               get_eaid_var(model_wealth_pca))
) %>%
  mutate(pct_eaid_explained = 100 * (eaid_var_null_common - eaid_var) / eaid_var_null_common)

nrow(icc_data)
[1] 12150
nobs(modelmulti)
[1] 12150
print(wealth_var_compare)
# A tibble: 3 × 3
  model                          eaid_var pct_eaid_explained
  <chr>                             <dbl>              <dbl>
1 Null                             0.111                 0  
2 IPUMS wealth quintile            0.0947               14.6
3 Stratified PCA wealth quintile   0.0944               14.8
#get ICC for other covariates

modelmulti_cluster_nourban <- lmer(summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined +
                              factor(MCP) + (1|FQINSTID)+ (1| EAID),
                            data = individualandcluster)
library(performance)
icc(modelmulti_cluster_nourban)
# Intraclass Correlation Coefficient

    Adjusted ICC: 0.273
  Unadjusted ICC: 0.262
icc(modelmulti_cluster_nourban, by_group = TRUE)
# ICC by Group

Group    |   ICC
----------------
FQINSTID | 0.109
EAID     | 0.163
var_covariates <- as.data.frame(VarCorr(modelmulti_cluster_nourban))
eaid_var_covariates <- var_covariates$vcov[var_covariates$grp == "EAID"]

pct_eaid_explained_by_covariates <- 100 * (eaid_var_null - eaid_var_covariates) / eaid_var_null

cat(sprintf(
  "Percent of between-EA variance explained by full individual covariate set: %.1f%%\n",
  pct_eaid_explained_by_covariates
))
Percent of between-EA variance explained by full individual covariate set: 12.1%
#cumulative between-EA variance explained: covariates, + wealth, + urban
cumulative_formulas <- list(
  "Covariates"                           = summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined + factor(MCP),
  "Covariates + IPUMS wealth"            = summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined + factor(MCP) + factor(WEALTHQ),
  "Covariates + IPUMS wealth + urban"    = summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined + factor(MCP) + factor(WEALTHQ) + factor(URBAN),
  "Covariates + Rutstein wealth"         = summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined + factor(MCP) + ncombsco,
  "Covariates + Rutstein wealth + urban" = summary_score_av ~ BIRTHEVENT + educationlevel + agegroup + maritalcombined + factor(MCP) + ncombsco + factor(URBAN)
)

cumulative_models <- lapply(cumulative_formulas, function(f) {
  lmer(update(f, . ~ . + (1 | FQINSTID) + (1 | EAID)), data = icc_data)
})

cumulative_var <- tibble(
  model    = c("Null", names(cumulative_models)),
  eaid_var = c(eaid_var_null_common, sapply(cumulative_models, get_eaid_var))
) %>%
  mutate(pct_eaid_explained = 100 * (eaid_var_null_common - eaid_var) / eaid_var_null_common)

print(cumulative_var)
# A tibble: 6 × 3
  model                                eaid_var pct_eaid_explained
  <chr>                                   <dbl>              <dbl>
1 Null                                   0.111                 0  
2 Covariates                             0.0974               12.1
3 Covariates + IPUMS wealth              0.0925               16.5
4 Covariates + IPUMS wealth + urban      0.0926               16.4
5 Covariates + Rutstein wealth           0.0920               17.0
6 Covariates + Rutstein wealth + urban   0.0922               16.8