Packages
# Only widely-available, stable packages
needed <- c("tidyverse",'rio',"knitr", "broom", "patchwork", "scales", "corrplot")
to_get <- needed[!needed %in% rownames(installed.packages())]
if (length(to_get)) install.packages(to_get, repos = "https://cloud.r-project.org")
library(tidyverse)
library(knitr)
library(rio)
library(broom)
library(patchwork)
library(scales)
library(corrplot)
Research
Objectives
This study uses dd (n = 5,000) to examine
psychological well-being across urban and non-urban
communities.
Outcome: High life satisfaction =
life_satisfaction >= 7 (binary, 0/1)
Objectives:
- Describe the sample by residence (Table 1)
- Estimate well-being prevalence across subgroups (Table 2)
- Identify determinants via logistic regression — urban / non-urban /
total (Table 3)
- Predict well-being probabilities by sex × income × residence (Table
4)
- Decompose the urban–non-urban gap (Blinder-Oaxaca, Tables 5–6)
Step 1 — Load and
Recode Data
# ── dd must already be in your environment ──────────────────────────────────
dd=import('https://raw.githubusercontent.com/datalake101/hssSampleData/refs/heads/main/healthData.csv')
df <- dd %>%
mutate(
# Outcome
high_wb = as.integer(life_satisfaction >= 7),
# Residence: 2-level
res2 = if_else(region == "Urban", "Urban", "Non-urban"),
res2 = factor(res2, levels = c("Urban", "Non-urban")),
# Age groups
age_grp = case_when(
age <= 24 ~ "18-24",
age <= 44 ~ "25-44",
age <= 64 ~ "45-64",
TRUE ~ "65+"
),
age_grp = factor(age_grp, levels = c("18-24","25-44","45-64","65+")),
# Education collapsed
edu = case_when(
education %in% c("None","Primary") ~ "None/Primary",
education == "Secondary" ~ "Secondary",
education %in% c("Vocational","Graduate") ~ "Tertiary",
education == "Postgrad" ~ "Postgraduate"
),
edu = factor(edu, levels = c("None/Primary","Secondary","Tertiary","Postgraduate")),
# Income tertile
inc3 = factor(ntile(monthly_income, 3), labels = c("Low","Medium","High")),
# Marital
mar = case_when(
marital_status == "Married" ~ "Married",
marital_status == "Partnered" ~ "Partnered",
marital_status == "Single" ~ "Single",
marital_status %in% c("Divorced","Widowed") ~ "Div/Widowed"
),
mar = factor(mar, levels = c("Married","Partnered","Single","Div/Widowed")),
# Sex
sex = factor(sex, levels = c("Male","Female","Non-binary")),
# Social capital composite (1-5 scale items)
sc_index = rowMeans(cbind(community_belonging, neighbor_trust,
family_support, friend_support,
perceived_cohesion), na.rm = TRUE),
sc_cat = case_when(
sc_index < 2 ~ "Low",
sc_index < 3.5 ~ "Medium",
TRUE ~ "High"
),
sc_cat = factor(sc_cat, levels = c("Low","Medium","High")),
# Loneliness
lone_cat = case_when(
loneliness_frequency <= 2 ~ "Low",
loneliness_frequency <= 3 ~ "Moderate",
TRUE ~ "High"
),
lone_cat = factor(lone_cat, levels = c("Low","Moderate","High")),
# Screen time
screen_cat = case_when(
daily_screen_time < 2 ~ "Low",
daily_screen_time < 5 ~ "Moderate",
TRUE ~ "High"
),
screen_cat = factor(screen_cat, levels = c("Low","Moderate","High")),
# Mindfulness
mind_cat = case_when(
mindfulness_practice %in% c("Never","Rarely") ~ "Infrequent",
mindfulness_practice == "Sometimes" ~ "Sometimes",
TRUE ~ "Regular"
),
mind_cat = factor(mind_cat, levels = c("Infrequent","Sometimes","Regular")),
# Simple Yes/No factors
can_confide = factor(can_confide, levels = c("No","Yes")),
club_member = factor(club_member, levels = c("No","Yes")),
voted = factor(voted_last_election, levels = c("No","Yes")),
donated = factor(donated_charity, levels = c("No","Yes")),
uses_sm = factor(uses_social_media, levels = c("No","Yes")),
shares_news = factor(shares_news, levels = c("No","Yes")),
climate_sm = factor(climate_info_on_sm, levels = c("No","Yes")),
therapy = factor(recent_therapy, levels = c("No","Yes")),
discrim = factor(experienced_discrimination, levels = c("No","Yes")),
foreign_born = factor(foreign_born, levels = c("No","Yes")),
ethnicity = factor(ethnicity,
levels = c("White","Black","Latino",
"Asian","Indigenous","Other")),
# Volunteerism
vol_cat = case_when(
volunteerism == "Never" ~ "Never",
volunteerism == "Rarely" ~ "Rarely",
volunteerism == "Sometimes" ~ "Sometimes",
TRUE ~ "Often"
),
vol_cat = factor(vol_cat, levels = c("Never","Rarely","Sometimes","Often"))
)
cat("Rows:", nrow(df), "\n")
## Rows: 5000
cat("High well-being:", sum(df$high_wb, na.rm=TRUE),
"(", round(mean(df$high_wb, na.rm=TRUE)*100, 1), "%)\n")
## High well-being: 2020 ( 40.4 %)
cat("Urban:", sum(df$res2=="Urban"),
"| Non-urban:", sum(df$res2=="Non-urban"), "\n")
## Urban: 1242 | Non-urban: 3758
Step 2 — Table 1:
Sample Description by Residence
# Simple frequency table function
freq_by_res <- function(var_name) {
data.frame(
Category = levels(df[[var_name]]),
Overall = paste0(table(df[[var_name]]),
" (", round(prop.table(table(df[[var_name]]))*100, 1), "%)"),
Urban = paste0(table(df[[var_name]][df$res2=="Urban"]),
" (", round(prop.table(table(df[[var_name]][df$res2=="Urban"]))*100, 1), "%)"),
Non_urban = paste0(table(df[[var_name]][df$res2=="Non-urban"]),
" (", round(prop.table(table(df[[var_name]][df$res2=="Non-urban"]))*100, 1), "%)")
)
}
vars_desc <- c("age_grp","sex","edu","inc3","mar",
"sc_cat","lone_cat","screen_cat","mind_cat",
"can_confide","club_member","voted","donated",
"uses_sm","shares_news","discrim","therapy")
# Build table with section headers
t1_list <- lapply(vars_desc, function(v) {
header <- data.frame(Category = paste0("-- ", v, " --"),
Overall = "", Urban = "", Non_urban = "")
body <- freq_by_res(v)
rbind(header, body)
})
t1 <- do.call(rbind, t1_list)
rownames(t1) <- NULL
# Continuous summaries at top
cont_sum <- data.frame(
Category = c("-- Age (mean, SD) --",
"Age",
"-- Social Capital Index (mean, SD) --",
"Social Capital Index"),
Overall = c("", paste0(round(mean(df$age, na.rm=TRUE),1),
" (", round(sd(df$age, na.rm=TRUE),1), ")"),
"", paste0(round(mean(df$sc_index, na.rm=TRUE),1),
" (", round(sd(df$sc_index, na.rm=TRUE),1), ")")),
Urban = c("", paste0(round(mean(df$age[df$res2=="Urban"], na.rm=TRUE),1),
" (", round(sd(df$age[df$res2=="Urban"], na.rm=TRUE),1), ")"),
"", paste0(round(mean(df$sc_index[df$res2=="Urban"], na.rm=TRUE),1),
" (", round(sd(df$sc_index[df$res2=="Urban"], na.rm=TRUE),1), ")")),
Non_urban = c("", paste0(round(mean(df$age[df$res2=="Non-urban"], na.rm=TRUE),1),
" (", round(sd(df$age[df$res2=="Non-urban"], na.rm=TRUE),1), ")"),
"", paste0(round(mean(df$sc_index[df$res2=="Non-urban"], na.rm=TRUE),1),
" (", round(sd(df$sc_index[df$res2=="Non-urban"], na.rm=TRUE),1), ")"))
)
t1_full <- rbind(cont_sum, t1)
kable(t1_full,
caption = "Table 1. Sample Characteristics by Residence",
col.names = c("Variable / Category", "Overall", "Urban", "Non-urban"),
row.names = FALSE)
Table 1. Sample Characteristics by Residence
| – Age (mean, SD) – |
|
|
|
| Age |
53.8 (20.7) |
54.8 (20.3) |
53.5 (20.8) |
| – Social Capital Index (mean, SD) – |
|
|
|
| Social Capital Index |
3 (0.6) |
3 (0.6) |
3 (0.6) |
| – age_grp – |
|
|
|
| 18-24 |
436 (8.7%) |
85 (6.8%) |
351 (9.3%) |
| 25-44 |
1420 (28.4%) |
346 (27.9%) |
1074 (28.6%) |
| 45-64 |
1436 (28.7%) |
381 (30.7%) |
1055 (28.1%) |
| 65+ |
1708 (34.2%) |
430 (34.6%) |
1278 (34%) |
| – sex – |
|
|
|
| Male |
2337 (46.7%) |
587 (47.3%) |
1750 (46.6%) |
| Female |
2603 (52.1%) |
642 (51.7%) |
1961 (52.2%) |
| Non-binary |
60 (1.2%) |
13 (1%) |
47 (1.3%) |
| – edu – |
|
|
|
| None/Primary |
1666 (33.3%) |
409 (32.9%) |
1257 (33.4%) |
| Secondary |
817 (16.3%) |
198 (15.9%) |
619 (16.5%) |
| Tertiary |
1675 (33.5%) |
428 (34.5%) |
1247 (33.2%) |
| Postgraduate |
842 (16.8%) |
207 (16.7%) |
635 (16.9%) |
| – inc3 – |
|
|
|
| Low |
1667 (33.3%) |
415 (33.4%) |
1252 (33.3%) |
| Medium |
1667 (33.3%) |
409 (32.9%) |
1258 (33.5%) |
| High |
1666 (33.3%) |
418 (33.7%) |
1248 (33.2%) |
| – mar – |
|
|
|
| Married |
998 (20%) |
254 (20.5%) |
744 (19.8%) |
| Partnered |
1019 (20.4%) |
241 (19.4%) |
778 (20.7%) |
| Single |
988 (19.8%) |
247 (19.9%) |
741 (19.7%) |
| Div/Widowed |
1995 (39.9%) |
500 (40.3%) |
1495 (39.8%) |
| – sc_cat – |
|
|
|
| Low |
216 (4.3%) |
57 (4.6%) |
159 (4.2%) |
| Medium |
3667 (73.3%) |
896 (72.1%) |
2771 (73.7%) |
| High |
1117 (22.3%) |
289 (23.3%) |
828 (22%) |
| – lone_cat – |
|
|
|
| Low |
2069 (41.4%) |
486 (39.1%) |
1583 (42.1%) |
| Moderate |
960 (19.2%) |
246 (19.8%) |
714 (19%) |
| High |
1971 (39.4%) |
510 (41.1%) |
1461 (38.9%) |
| – screen_cat – |
|
|
|
| Low |
1095 (21.9%) |
256 (20.6%) |
839 (22.3%) |
| Moderate |
1628 (32.6%) |
397 (32%) |
1231 (32.8%) |
| High |
2277 (45.5%) |
589 (47.4%) |
1688 (44.9%) |
| – mind_cat – |
|
|
|
| Infrequent |
2030 (40.6%) |
488 (39.3%) |
1542 (41%) |
| Sometimes |
943 (18.9%) |
237 (19.1%) |
706 (18.8%) |
| Regular |
2027 (40.5%) |
517 (41.6%) |
1510 (40.2%) |
| – can_confide – |
|
|
|
| No |
1270 (25.4%) |
314 (25.3%) |
956 (25.4%) |
| Yes |
3730 (74.6%) |
928 (74.7%) |
2802 (74.6%) |
| – club_member – |
|
|
|
| No |
3835 (76.7%) |
923 (74.3%) |
2912 (77.5%) |
| Yes |
1165 (23.3%) |
319 (25.7%) |
846 (22.5%) |
| – voted – |
|
|
|
| No |
1704 (34.1%) |
424 (34.1%) |
1280 (34.1%) |
| Yes |
3296 (65.9%) |
818 (65.9%) |
2478 (65.9%) |
| – donated – |
|
|
|
| No |
3575 (71.5%) |
882 (71%) |
2693 (71.7%) |
| Yes |
1425 (28.5%) |
360 (29%) |
1065 (28.3%) |
| – uses_sm – |
|
|
|
| No |
735 (14.7%) |
168 (13.5%) |
567 (15.1%) |
| Yes |
4265 (85.3%) |
1074 (86.5%) |
3191 (84.9%) |
| – shares_news – |
|
|
|
| No |
4053 (81.1%) |
1023 (82.4%) |
3030 (80.6%) |
| Yes |
947 (18.9%) |
219 (17.6%) |
728 (19.4%) |
| – discrim – |
|
|
|
| No |
4608 (92.2%) |
1153 (92.8%) |
3455 (91.9%) |
| Yes |
392 (7.8%) |
89 (7.2%) |
303 (8.1%) |
| – therapy – |
|
|
|
| No |
4092 (81.8%) |
1004 (80.8%) |
3088 (82.2%) |
| Yes |
908 (18.2%) |
238 (19.2%) |
670 (17.8%) |
Step 3 — Table 2:
Prevalence of High Well-being by Subgroup
# Function: prevalence (%) by one variable and by residence, with chi-sq p
prev_by_var <- function(var_name) {
d <- df[!is.na(df[[var_name]]) & !is.na(df$high_wb), ]
# Overall prevalence per category
ov <- tapply(d$high_wb, d[[var_name]], function(x) round(mean(x)*100, 1))
# Urban
du <- d[d$res2 == "Urban",]
ur <- tapply(du$high_wb, du[[var_name]], function(x) round(mean(x)*100, 1))
# Non-urban
dn <- d[d$res2 == "Non-urban",]
nu <- tapply(dn$high_wb, dn[[var_name]], function(x) round(mean(x)*100, 1))
# Chi-square p (variable vs high_wb, ignore residence)
p <- round(chisq.test(table(d[[var_name]], d$high_wb))$p.value, 4)
p_fmt <- if (p < 0.001) "<0.001" else as.character(p)
cats <- levels(d[[var_name]])
out <- data.frame(
Category = cats,
Overall = as.numeric(ov[cats]),
Urban = as.numeric(ur[cats]),
Non_urban = as.numeric(nu[cats]),
p_value = c(p_fmt, rep("", length(cats)-1)),
stringsAsFactors = FALSE
)
header <- data.frame(Category = paste0("-- ", var_name, " --"),
Overall = NA, Urban = NA, Non_urban = NA,
p_value = "", stringsAsFactors = FALSE)
rbind(header, out)
}
vars_t2 <- c("age_grp","sex","edu","inc3","mar",
"sc_cat","lone_cat","screen_cat","mind_cat",
"can_confide","club_member","voted","donated",
"uses_sm","discrim","therapy")
t2 <- do.call(rbind, lapply(vars_t2, prev_by_var))
rownames(t2) <- NULL
kable(t2,
caption = "Table 2. Prevalence of High Well-being (%) by Subgroup and Residence",
col.names = c("Variable / Category", "Overall (%)", "Urban (%)",
"Non-urban (%)", "p-value"),
na.string = "")
Table 2. Prevalence of High Well-being (%) by Subgroup and
Residence
| – age_grp – |
NA |
NA |
NA |
|
| 18-24 |
42.2 |
43.5 |
41.9 |
0.8519 |
| 25-44 |
40.6 |
41.6 |
40.3 |
|
| 45-64 |
40.1 |
37.0 |
41.2 |
|
| 65+ |
40.0 |
38.1 |
40.6 |
|
| – sex – |
NA |
NA |
NA |
|
| Male |
40.1 |
37.1 |
41.0 |
0.4477 |
| Female |
40.9 |
40.8 |
40.9 |
|
| Non-binary |
33.3 |
46.2 |
29.8 |
|
| – edu – |
NA |
NA |
NA |
|
| None/Primary |
39.4 |
36.9 |
40.2 |
0.3206 |
| Secondary |
39.9 |
41.9 |
39.3 |
|
| Tertiary |
42.2 |
40.2 |
42.9 |
|
| Postgraduate |
39.3 |
38.6 |
39.5 |
|
| – inc3 – |
NA |
NA |
NA |
|
| Low |
40.9 |
37.8 |
41.9 |
0.8159 |
| Medium |
39.8 |
36.2 |
41.0 |
|
| High |
40.5 |
43.3 |
39.5 |
|
| – mar – |
NA |
NA |
NA |
|
| Married |
40.7 |
40.6 |
40.7 |
0.6529 |
| Partnered |
42.0 |
40.7 |
42.4 |
|
| Single |
39.8 |
34.4 |
41.6 |
|
| Div/Widowed |
39.7 |
40.0 |
39.7 |
|
| – sc_cat – |
NA |
NA |
NA |
|
| Low |
38.9 |
26.3 |
43.4 |
0.5975 |
| Medium |
40.1 |
40.0 |
40.2 |
|
| High |
41.6 |
39.1 |
42.5 |
|
| – lone_cat – |
NA |
NA |
NA |
|
| Low |
40.5 |
39.7 |
40.7 |
0.9357 |
| Moderate |
39.9 |
38.2 |
40.5 |
|
| High |
40.6 |
39.0 |
41.1 |
|
| – screen_cat – |
NA |
NA |
NA |
|
| Low |
39.8 |
37.5 |
40.5 |
0.9022 |
| Moderate |
40.5 |
40.6 |
40.5 |
|
| High |
40.6 |
38.9 |
41.2 |
|
| – mind_cat – |
NA |
NA |
NA |
|
| Infrequent |
39.5 |
34.8 |
40.9 |
0.0961 |
| Sometimes |
38.6 |
38.8 |
38.5 |
|
| Regular |
42.2 |
43.3 |
41.8 |
|
| – can_confide – |
NA |
NA |
NA |
|
| No |
39.5 |
38.9 |
39.7 |
0.4836 |
| Yes |
40.7 |
39.2 |
41.2 |
|
| – club_member – |
NA |
NA |
NA |
|
| No |
39.9 |
39.2 |
40.1 |
0.1762 |
| Yes |
42.1 |
38.9 |
43.4 |
|
| – voted – |
NA |
NA |
NA |
|
| No |
39.1 |
35.1 |
40.5 |
0.2034 |
| Yes |
41.0 |
41.2 |
41.0 |
|
| – donated – |
NA |
NA |
NA |
|
| No |
40.7 |
39.8 |
41.0 |
0.5149 |
| Yes |
39.6 |
37.5 |
40.4 |
|
| – uses_sm – |
NA |
NA |
NA |
|
| No |
41.5 |
38.1 |
42.5 |
0.5384 |
| Yes |
40.2 |
39.3 |
40.5 |
|
| – discrim – |
NA |
NA |
NA |
|
| No |
40.2 |
38.9 |
40.6 |
0.3833 |
| Yes |
42.6 |
41.6 |
42.9 |
|
| – therapy – |
NA |
NA |
NA |
|
| No |
40.5 |
38.1 |
41.2 |
0.8616 |
| Yes |
40.1 |
43.3 |
39.0 |
|
Step 5 — Table 3:
Logistic Regression (3 Models)
fml <- high_wb ~ sex + age_grp + edu + inc3 + mar + foreign_born +
ethnicity + sc_cat + can_confide + club_member +
vol_cat + voted + donated + uses_sm +
screen_cat + shares_news + climate_sm +
lone_cat + mind_cat + therapy + discrim
# Model I: Non-urban only
m_nu <- glm(fml, data = filter(df, res2=="Non-urban"), family=binomial())
# Model II: Urban only
m_ur <- glm(fml, data = filter(df, res2=="Urban"), family=binomial())
# Model III: Total, add residence
fml_tot <- update(fml, . ~ . + res2)
m_tot <- glm(fml_tot, data=df, family=binomial())
cat("N Non-urban:", nobs(m_nu), " | N Urban:", nobs(m_ur),
" | N Total:", nobs(m_tot), "\n")
## N Non-urban: 3758 | N Urban: 1242 | N Total: 5000
# Helper: OR table with stars
or_table <- function(mod) {
tidy(mod, conf.int=TRUE, exponentiate=TRUE) %>%
filter(term != "(Intercept)") %>%
mutate(
star = case_when(p.value<0.001~"***", p.value<0.01~"**",
p.value<0.05~"*", TRUE~""),
cell = paste0(sprintf("%.2f",estimate),
" [", sprintf("%.2f",conf.low),
"-", sprintf("%.2f",conf.high), "]", star)
) %>%
select(term, cell)
}
t3_nu <- or_table(m_nu) %>% rename(`Non-urban` = cell)
t3_ur <- or_table(m_ur) %>% rename(Urban = cell)
t3_tot <- or_table(m_tot) %>% rename(Total = cell)
t3 <- t3_nu %>%
full_join(t3_ur, by="term") %>%
full_join(t3_tot, by="term") %>%
# readable term labels
mutate(term = term %>%
str_replace("^sex", "Sex: ") %>%
str_replace("^age_grp", "Age: ") %>%
str_replace("^edu", "Education: ") %>%
str_replace("^inc3", "Income: ") %>%
str_replace("^mar", "Marital: ") %>%
str_replace("^foreign_born", "Foreign Born: ") %>%
str_replace("^ethnicity", "Ethnicity: ") %>%
str_replace("^sc_cat", "Social Capital: ") %>%
str_replace("^can_confide", "Can Confide: ") %>%
str_replace("^club_member", "Club Member: ") %>%
str_replace("^vol_cat", "Volunteering: ") %>%
str_replace("^voted", "Voted: ") %>%
str_replace("^donated", "Donated: ") %>%
str_replace("^uses_sm", "Uses Social Media: ") %>%
str_replace("^screen_cat", "Screen Time: ") %>%
str_replace("^shares_news", "Shares News: ") %>%
str_replace("^climate_sm", "Climate Info SM: ") %>%
str_replace("^lone_cat", "Loneliness: ") %>%
str_replace("^mind_cat", "Mindfulness: ") %>%
str_replace("^therapy", "Therapy: ") %>%
str_replace("^discrim", "Discrimination: ") %>%
str_replace("^res2", "Residence: ")
)
kable(t3,
caption = "Table 3. Adjusted Odds Ratios [95% CI] for High Well-being",
col.names = c("Predictor","Non-urban AOR [95%CI]",
"Urban AOR [95%CI]","Total AOR [95%CI]"),
align = "lrrr") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped","condensed"), full_width=TRUE, font_size=12)
Table 3. Adjusted Odds Ratios [95% CI] for High Well-being
|
Predictor
|
Non-urban AOR [95%CI]
|
Urban AOR [95%CI]
|
Total AOR [95%CI]
|
|
Sex: Female
|
0.99 [0.87-1.13]
|
1.19 [0.94-1.51]
|
1.04 [0.92-1.16]
|
|
Sex: Non-binary
|
0.59 [0.30-1.09]
|
1.81 [0.56-5.75]
|
0.73 [0.41-1.24]
|
|
Age: 25-44
|
0.94 [0.73-1.20]
|
0.87 [0.53-1.44]
|
0.94 [0.76-1.17]
|
|
Age: 45-64
|
0.97 [0.76-1.25]
|
0.71 [0.43-1.16]
|
0.93 [0.74-1.15]
|
|
Age: 65+
|
0.94 [0.74-1.20]
|
0.76 [0.47-1.25]
|
0.91 [0.73-1.13]
|
|
Education: Secondary
|
0.96 [0.79-1.17]
|
1.24 [0.86-1.77]
|
1.02 [0.86-1.21]
|
|
Education: Tertiary
|
1.11 [0.95-1.31]
|
1.17 [0.88-1.57]
|
1.12 [0.98-1.29]
|
|
Education: Postgraduate
|
0.97 [0.80-1.19]
|
1.11 [0.78-1.58]
|
1.00 [0.84-1.19]
|
|
Income: Medium
|
0.96 [0.82-1.13]
|
0.90 [0.67-1.21]
|
0.96 [0.83-1.10]
|
|
Income: High
|
0.91 [0.78-1.07]
|
1.29 [0.97-1.72]
|
0.99 [0.86-1.14]
|
|
Marital: Partnered
|
1.08 [0.88-1.33]
|
0.98 [0.68-1.43]
|
1.07 [0.89-1.28]
|
|
Marital: Single
|
1.03 [0.83-1.27]
|
0.82 [0.57-1.20]
|
0.97 [0.81-1.16]
|
|
Marital: Div/Widowed
|
0.95 [0.80-1.14]
|
1.02 [0.74-1.40]
|
0.97 [0.83-1.13]
|
|
Foreign Born: Yes
|
0.90 [0.74-1.09]
|
1.02 [0.72-1.44]
|
0.94 [0.79-1.10]
|
|
Ethnicity: Black
|
1.01 [0.80-1.27]
|
0.92 [0.61-1.41]
|
0.98 [0.80-1.19]
|
|
Ethnicity: Latino
|
0.96 [0.77-1.21]
|
0.99 [0.65-1.50]
|
0.97 [0.79-1.18]
|
|
Ethnicity: Asian
|
1.04 [0.83-1.31]
|
0.91 [0.60-1.37]
|
0.99 [0.81-1.21]
|
|
Ethnicity: Indigenous
|
0.95 [0.75-1.19]
|
1.18 [0.80-1.75]
|
1.01 [0.83-1.23]
|
|
Ethnicity: Other
|
1.14 [0.91-1.44]
|
0.95 [0.63-1.43]
|
1.08 [0.89-1.32]
|
|
Social Capital: Medium
|
0.89 [0.64-1.23]
|
1.99 [1.09-3.83]*
|
1.05 [0.80-1.40]
|
|
Social Capital: High
|
0.97 [0.69-1.38]
|
1.94 [1.03-3.84]*
|
1.12 [0.83-1.52]
|
|
Can Confide: Yes
|
1.07 [0.92-1.25]
|
1.03 [0.78-1.35]
|
1.05 [0.93-1.20]
|
|
Club Member: Yes
|
1.15 [0.99-1.35]
|
0.98 [0.75-1.29]
|
1.11 [0.97-1.26]
|
|
Volunteering: Rarely
|
1.04 [0.88-1.22]
|
1.22 [0.91-1.63]
|
1.07 [0.93-1.24]
|
|
Volunteering: Sometimes
|
0.98 [0.83-1.17]
|
0.89 [0.66-1.20]
|
0.96 [0.83-1.11]
|
|
Volunteering: Often
|
0.94 [0.75-1.18]
|
0.91 [0.58-1.40]
|
0.93 [0.76-1.13]
|
|
Voted: Yes
|
1.03 [0.89-1.18]
|
1.37 [1.07-1.77]*
|
1.09 [0.97-1.23]
|
|
Donated: Yes
|
0.98 [0.85-1.14]
|
0.90 [0.69-1.17]
|
0.97 [0.85-1.10]
|
|
Uses Social Media: Yes
|
0.92 [0.76-1.10]
|
1.08 [0.77-1.53]
|
0.95 [0.81-1.11]
|
|
Screen Time: Moderate
|
0.99 [0.83-1.18]
|
1.17 [0.84-1.63]
|
1.03 [0.88-1.20]
|
|
Screen Time: High
|
1.03 [0.87-1.21]
|
1.07 [0.79-1.47]
|
1.04 [0.89-1.20]
|
|
Shares News: Yes
|
0.95 [0.80-1.12]
|
0.95 [0.70-1.29]
|
0.95 [0.82-1.10]
|
|
Climate Info SM: Yes
|
1.12 [0.95-1.31]
|
1.03 [0.78-1.38]
|
1.09 [0.95-1.26]
|
|
Loneliness: Moderate
|
0.99 [0.83-1.19]
|
0.90 [0.65-1.25]
|
0.97 [0.83-1.14]
|
|
Loneliness: High
|
1.03 [0.89-1.19]
|
0.97 [0.74-1.26]
|
1.01 [0.89-1.15]
|
|
Mindfulness: Sometimes
|
0.91 [0.76-1.09]
|
1.24 [0.89-1.73]
|
0.97 [0.83-1.14]
|
|
Mindfulness: Regular
|
1.04 [0.90-1.20]
|
1.50 [1.16-1.95]**
|
1.13 [1.00-1.28]
|
|
Therapy: Yes
|
0.91 [0.77-1.08]
|
1.25 [0.92-1.68]
|
0.99 [0.85-1.15]
|
|
Discrimination: Yes
|
1.09 [0.86-1.39]
|
1.08 [0.68-1.69]
|
1.10 [0.89-1.35]
|
|
Residence: Non-urban
|
NA
|
NA
|
1.08 [0.95-1.23]
|
Model fit
kable(data.frame(
Model = c("Non-urban","Urban","Total"),
N = c(nobs(m_nu), nobs(m_ur), nobs(m_tot)),
AIC = round(c(AIC(m_nu), AIC(m_ur), AIC(m_tot)), 1),
R2_McFad = round(1 - c(m_nu$deviance/m_nu$null.deviance,
m_ur$deviance/m_ur$null.deviance,
m_tot$deviance/m_tot$null.deviance), 3)
), caption="Model Fit", col.names=c("Model","N","AIC","McFadden R2"))
Model Fit
| Non-urban |
3758 |
5134.5 |
0.005 |
| Urban |
1242 |
1698.3 |
0.027 |
| Total |
5000 |
6799.7 |
0.004 |
Step 6 — Figure 2:
Forest Plot (Total Model)
tidy(m_tot, conf.int=TRUE, exponentiate=TRUE) %>%
filter(term != "(Intercept)") %>%
mutate(
sig = p.value < 0.05,
term = term %>%
str_replace("^sex","Sex: ") %>%
str_replace("^age_grp","Age: ") %>%
str_replace("^edu","Education: ") %>%
str_replace("^inc3","Income: ") %>%
str_replace("^mar","Marital: ") %>%
str_replace("^foreign_born","Foreign Born: ") %>%
str_replace("^ethnicity","Ethnicity: ") %>%
str_replace("^sc_cat","Social Capital: ") %>%
str_replace("^can_confide","Can Confide: ") %>%
str_replace("^club_member","Club Member: ") %>%
str_replace("^vol_cat","Volunteering: ") %>%
str_replace("^voted","Voted: ") %>%
str_replace("^donated","Donated: ") %>%
str_replace("^uses_sm","Uses Social Media: ") %>%
str_replace("^screen_cat","Screen Time: ") %>%
str_replace("^shares_news","Shares News: ") %>%
str_replace("^climate_sm","Climate Info SM: ") %>%
str_replace("^lone_cat","Loneliness: ") %>%
str_replace("^mind_cat","Mindfulness: ") %>%
str_replace("^therapy","Therapy: ") %>%
str_replace("^discrim","Discrimination: ") %>%
str_replace("^res2","Residence: ")
) %>%
ggplot(aes(x=estimate, y=reorder(term,estimate), colour=sig)) +
geom_vline(xintercept=1, linetype="dashed", colour="grey50") +
geom_errorbarh(aes(xmin=conf.low, xmax=conf.high), height=0.3) +
geom_point(aes(shape=sig), size=3) +
scale_x_log10() +
scale_colour_manual(values=c("TRUE"="#2C3E50","FALSE"="#BDC3C7"),
labels=c("TRUE"="p<0.05","FALSE"="ns"), name=NULL) +
scale_shape_manual(values=c("TRUE"=16,"FALSE"=1),
labels=c("TRUE"="p<0.05","FALSE"="ns"), name=NULL) +
labs(title="Figure 2. Adjusted Odds Ratios — Total Model",
x="AOR (log scale)", y=NULL,
caption="Error bars = 95% CI. Dashed line = OR of 1.") +
theme_minimal(base_size=11) +
theme(legend.position="top", panel.grid.minor=element_blank())

Step 7 — Table 4:
Predicted Probabilities (Sex × Income × Residence)
# Mode helper
mode_val <- function(x) {
ux <- unique(na.omit(x))
ux[which.max(tabulate(match(x, ux)))]
}
# Build reference grid
grid <- expand.grid(
sex = c("Male","Female"),
inc3 = c("Low","High"),
res2 = c("Urban","Non-urban"),
stringsAsFactors = FALSE
) %>%
mutate(
age_grp = mode_val(df$age_grp) %>% as.character(),
edu = mode_val(df$edu) %>% as.character(),
mar = mode_val(df$mar) %>% as.character(),
foreign_born = mode_val(df$foreign_born) %>% as.character(),
ethnicity = mode_val(df$ethnicity) %>% as.character(),
sc_cat = mode_val(df$sc_cat) %>% as.character(),
can_confide = mode_val(df$can_confide) %>% as.character(),
club_member = mode_val(df$club_member) %>% as.character(),
vol_cat = mode_val(df$vol_cat) %>% as.character(),
voted = mode_val(df$voted) %>% as.character(),
donated = mode_val(df$donated) %>% as.character(),
uses_sm = mode_val(df$uses_sm) %>% as.character(),
screen_cat = mode_val(df$screen_cat) %>% as.character(),
shares_news = mode_val(df$shares_news) %>% as.character(),
climate_sm = mode_val(df$climate_sm) %>% as.character(),
lone_cat = mode_val(df$lone_cat) %>% as.character(),
mind_cat = mode_val(df$mind_cat) %>% as.character(),
therapy = mode_val(df$therapy) %>% as.character(),
discrim = mode_val(df$discrim) %>% as.character()
)
# Match factor levels to df
for (col in names(grid)) {
if (col %in% names(df) && is.factor(df[[col]])) {
grid[[col]] <- factor(grid[[col]], levels = levels(df[[col]]))
}
}
p_fit <- predict(m_tot, newdata=grid, type="response", se.fit=TRUE)
t4 <- grid %>%
select(sex, inc3, res2) %>%
mutate(
Probability = round(p_fit$fit, 3),
Percent = paste0(round(p_fit$fit * 100, 1), "%"),
CI_95 = paste0("[",
round((p_fit$fit - 1.96*p_fit$se.fit)*100, 1),
"% - ",
round((p_fit$fit + 1.96*p_fit$se.fit)*100, 1),
"%]")
)
kable(t4,
caption = "Table 4. Predicted Probability of High Well-being",
col.names = c("Sex","Income","Residence","Probability","Percent","95% CI"))
Table 4. Predicted Probability of High Well-being
| Male |
Low |
Urban |
0.388 |
38.8% |
[32% - 45.7%] |
| Female |
Low |
Urban |
0.397 |
39.7% |
[32.7% - 46.6%] |
| Male |
High |
Urban |
0.386 |
38.6% |
[31.7% - 45.4%] |
| Female |
High |
Urban |
0.394 |
39.4% |
[32.5% - 46.3%] |
| Male |
Low |
Non-urban |
0.407 |
40.7% |
[34.1% - 47.3%] |
| Female |
Low |
Non-urban |
0.415 |
41.5% |
[34.9% - 48.1%] |
| Male |
High |
Non-urban |
0.404 |
40.4% |
[33.9% - 47%] |
| Female |
High |
Non-urban |
0.413 |
41.3% |
[34.7% - 47.8%] |
Step 8 —
Blinder-Oaxaca Decomposition (Tables 5 & 6)
We implement the threefold decomposition manually
using linear probability models (LPM), which avoids any dependency on
the oaxaca package.
# Dummy-code all predictors
df_d <- df %>%
filter(!is.na(high_wb)) %>%
mutate(
urban = as.integer(res2 == "Urban"),
d_female = as.integer(sex == "Female"),
d_nonbin = as.integer(sex == "Non-binary"),
d_a25 = as.integer(age_grp == "25-44"),
d_a45 = as.integer(age_grp == "45-64"),
d_a65 = as.integer(age_grp == "65+"),
d_edu_sec = as.integer(edu == "Secondary"),
d_edu_ter = as.integer(edu == "Tertiary"),
d_edu_post = as.integer(edu == "Postgraduate"),
d_inc_med = as.integer(inc3 == "Medium"),
d_inc_hi = as.integer(inc3 == "High"),
d_mar_part = as.integer(mar == "Partnered"),
d_mar_sing = as.integer(mar == "Single"),
d_mar_dw = as.integer(mar == "Div/Widowed"),
d_foreign = as.integer(foreign_born == "Yes"),
d_sc_med = as.integer(sc_cat == "Medium"),
d_sc_hi = as.integer(sc_cat == "High"),
d_confide = as.integer(can_confide == "Yes"),
d_club = as.integer(club_member == "Yes"),
d_vol_rare = as.integer(vol_cat == "Rarely"),
d_vol_some = as.integer(vol_cat == "Sometimes"),
d_vol_often = as.integer(vol_cat == "Often"),
d_voted = as.integer(voted == "Yes"),
d_donated = as.integer(donated == "Yes"),
d_sm = as.integer(uses_sm == "Yes"),
d_scr_mod = as.integer(screen_cat == "Moderate"),
d_scr_hi = as.integer(screen_cat == "High"),
d_news = as.integer(shares_news == "Yes"),
d_climate = as.integer(climate_sm == "Yes"),
d_lone_mod = as.integer(lone_cat == "Moderate"),
d_lone_hi = as.integer(lone_cat == "High"),
d_mind_some = as.integer(mind_cat == "Sometimes"),
d_mind_reg = as.integer(mind_cat == "Regular"),
d_therapy = as.integer(therapy == "Yes"),
d_discrim = as.integer(discrim == "Yes")
)
# LPM formula (same predictors)
lpm_fml <- high_wb ~ d_female + d_nonbin +
d_a25 + d_a45 + d_a65 +
d_edu_sec + d_edu_ter + d_edu_post +
d_inc_med + d_inc_hi +
d_mar_part + d_mar_sing + d_mar_dw +
d_foreign +
d_sc_med + d_sc_hi + d_confide + d_club +
d_vol_rare + d_vol_some + d_vol_often +
d_voted + d_donated +
d_sm + d_scr_mod + d_scr_hi + d_news + d_climate +
d_lone_mod + d_lone_hi +
d_mind_some + d_mind_reg +
d_therapy + d_discrim
df_u <- filter(df_d, urban == 1)
df_nu <- filter(df_d, urban == 0)
lpm_u <- lm(lpm_fml, data = df_u)
lpm_nu <- lm(lpm_fml, data = df_nu)
# Group means
mean_u <- mean(df_u$high_wb, na.rm=TRUE)
mean_nu <- mean(df_nu$high_wb, na.rm=TRUE)
gap <- mean_u - mean_nu
# Means of predictors in each group (exclude intercept)
X_u <- colMeans(model.matrix(lpm_fml, data=df_u) [, -1])
X_nu <- colMeans(model.matrix(lpm_fml, data=df_nu)[, -1])
# Coefficients (exclude intercept)
B_u <- coef(lpm_u) [-1]
B_nu <- coef(lpm_nu)[-1]
# Threefold decomposition
# E = endowments = (X_u - X_nu) * B_u
# C = coefficients = X_nu * (B_u - B_nu)
# CE = interaction = (X_u - X_nu) * (B_u - B_nu)
dX <- X_u - X_nu
dB <- B_u - B_nu
E_total <- sum(dX * B_u, na.rm=TRUE)
C_total <- sum(X_nu * dB, na.rm=TRUE)
CE_total <- sum(dX * dB, na.rm=TRUE)
cat("Gap:", round(gap,4),
" | E:", round(E_total,4),
" | C:", round(C_total,4),
" | CE:", round(CE_total,4),
" | Sum:", round(E_total+C_total+CE_total, 4), "\n")
## Gap: -0.0169 | E: -0.0011 | C: 0.2977 | CE: -0.0028 | Sum: 0.2938
Table 5 — Overall
Decomposition
t5 <- data.frame(
Component = c(
"Urban — mean probability",
"Non-urban — mean probability",
"Total difference (Urban minus Non-urban)",
" Endowments effect (explained)",
" Coefficients effect (unexplained)",
" Interaction effect"
),
Estimate = round(c(mean_u, mean_nu, gap,
E_total, C_total, CE_total), 4),
Pct_of_gap = round(c(mean_u*100, mean_nu*100, 100,
E_total/gap*100,
C_total/gap*100,
CE_total/gap*100), 1),
stringsAsFactors = FALSE
)
kable(t5,
caption = "Table 5. Blinder-Oaxaca Decomposition: Urban vs Non-urban Well-being Gap",
col.names = c("Component","Estimate","% of Total Gap"))
Table 5. Blinder-Oaxaca Decomposition: Urban vs Non-urban
Well-being Gap
| Urban — mean probability |
0.3913 |
39.1 |
| Non-urban — mean probability |
0.4082 |
40.8 |
| Total difference (Urban minus Non-urban) |
-0.0169 |
100.0 |
| Endowments effect (explained) |
-0.0011 |
6.5 |
| Coefficients effect (unexplained) |
0.2977 |
-1762.5 |
| Interaction effect |
-0.0028 |
16.8 |
Table 6 — Detailed
Decomposition by Predictor
# Variable-level contributions to the endowments effect
E_vars <- dX * B_u
# Clean labels
var_labels <- c(
d_female="Female (vs Male)", d_nonbin="Non-binary (vs Male)",
d_a25="Age 25-44", d_a45="Age 45-64", d_a65="Age 65+",
d_edu_sec="Secondary Educ.", d_edu_ter="Tertiary Educ.",
d_edu_post="Postgraduate Educ.",
d_inc_med="Income: Medium", d_inc_hi="Income: High",
d_mar_part="Marital: Partnered", d_mar_sing="Marital: Single",
d_mar_dw="Marital: Div/Widowed",
d_foreign="Foreign Born",
d_sc_med="Social Capital: Medium", d_sc_hi="Social Capital: High",
d_confide="Can Confide", d_club="Club Member",
d_vol_rare="Volunteering: Rarely", d_vol_some="Volunteering: Sometimes",
d_vol_often="Volunteering: Often",
d_voted="Voted", d_donated="Donated to Charity",
d_sm="Uses Social Media",
d_scr_mod="Screen Time: Moderate", d_scr_hi="Screen Time: High",
d_news="Shares News Online", d_climate="Shares Climate Info",
d_lone_mod="Loneliness: Moderate", d_lone_hi="Loneliness: High",
d_mind_some="Mindfulness: Sometimes", d_mind_reg="Mindfulness: Regular",
d_therapy="Recent Therapy", d_discrim="Experienced Discrimination"
)
t6 <- data.frame(
Variable = var_labels[names(E_vars)],
E_coef = round(as.numeric(E_vars), 5),
E_pct = round(as.numeric(E_vars) / gap * 100, 1),
C_coef = round(as.numeric(X_nu * dB), 5),
C_pct = round(as.numeric(X_nu * dB) / gap * 100, 1),
CE_coef = round(as.numeric(dX * dB), 5),
CE_pct = round(as.numeric(dX * dB) / gap * 100, 1),
stringsAsFactors = FALSE
)
# Sort by absolute endowments contribution
t6 <- t6[order(abs(t6$E_pct), decreasing=TRUE), ]
rownames(t6) <- NULL
kable(t6,
caption = "Table 6. Detailed Decomposition: Contribution of Each Predictor",
col.names = c("Variable",
"Endow. Coef","Endow. %",
"Coeff. Coef","Coeff. %",
"Inter. Coef","Inter. %"))
Table 6. Detailed Decomposition: Contribution of Each
Predictor
| Social Capital: Medium |
-0.00239 |
14.1 |
0.13057 |
-773.0 |
-0.00282 |
16.7 |
| Age 45-64 |
-0.00207 |
12.2 |
-0.02050 |
121.4 |
-0.00190 |
11.3 |
| Social Capital: High |
0.00179 |
-10.6 |
0.03314 |
-196.2 |
0.00186 |
-11.0 |
| Mindfulness: Regular |
0.00135 |
-8.0 |
0.03411 |
-201.9 |
0.00123 |
-7.3 |
| Recent Therapy |
0.00071 |
-4.2 |
0.01342 |
-79.5 |
0.00100 |
-5.9 |
| Tertiary Educ. |
0.00047 |
-2.8 |
0.00390 |
-23.1 |
0.00015 |
-0.9 |
| Volunteering: Sometimes |
-0.00042 |
2.5 |
-0.00586 |
34.7 |
-0.00037 |
2.2 |
| Age 65+ |
-0.00038 |
2.3 |
-0.01616 |
95.6 |
-0.00029 |
1.7 |
| Volunteering: Often |
0.00039 |
-2.3 |
-0.00057 |
3.4 |
0.00010 |
-0.6 |
| Screen Time: High |
0.00040 |
-2.3 |
0.00455 |
-27.0 |
0.00025 |
-1.5 |
| Volunteering: Rarely |
-0.00036 |
2.2 |
0.01030 |
-61.0 |
-0.00029 |
1.7 |
| Screen Time: Moderate |
-0.00029 |
1.7 |
0.01295 |
-76.7 |
-0.00031 |
1.9 |
| Non-binary (vs Male) |
-0.00027 |
1.6 |
0.00315 |
-18.7 |
-0.00051 |
3.0 |
| Income: High |
0.00028 |
-1.6 |
0.02772 |
-164.1 |
0.00037 |
-2.2 |
| Uses Social Media |
0.00026 |
-1.6 |
0.03160 |
-187.1 |
0.00058 |
-3.4 |
| Secondary Educ. |
-0.00026 |
1.5 |
0.00967 |
-57.2 |
-0.00031 |
1.8 |
| Age 25-44 |
0.00022 |
-1.3 |
-0.00470 |
27.8 |
0.00012 |
-0.7 |
| Shares News Online |
0.00022 |
-1.3 |
0.00012 |
-0.7 |
-0.00001 |
0.1 |
| Female (vs Male) |
-0.00020 |
1.2 |
0.02203 |
-130.4 |
-0.00021 |
1.2 |
| Donated to Charity |
-0.00017 |
1.0 |
-0.00652 |
38.6 |
-0.00015 |
0.9 |
| Loneliness: Moderate |
-0.00017 |
1.0 |
-0.00370 |
21.9 |
-0.00016 |
0.9 |
| Loneliness: High |
-0.00017 |
1.0 |
-0.00519 |
30.7 |
-0.00029 |
1.7 |
| Mindfulness: Sometimes |
0.00015 |
-0.9 |
0.01412 |
-83.6 |
0.00022 |
-1.3 |
| Experienced Discrimination |
-0.00013 |
0.8 |
-0.00062 |
3.7 |
0.00007 |
-0.4 |
| Income: Medium |
0.00013 |
-0.7 |
-0.00483 |
28.6 |
0.00008 |
-0.5 |
| Marital: Single |
-0.00008 |
0.5 |
-0.01022 |
60.5 |
-0.00009 |
0.5 |
| Club Member |
-0.00007 |
0.4 |
-0.00818 |
48.4 |
-0.00115 |
6.8 |
| Postgraduate Educ. |
-0.00005 |
0.3 |
0.00461 |
-27.3 |
-0.00006 |
0.4 |
| Marital: Partnered |
0.00005 |
-0.3 |
-0.00456 |
27.0 |
0.00029 |
-1.7 |
| Voted |
-0.00006 |
0.3 |
0.04516 |
-267.4 |
-0.00005 |
0.3 |
| Foreign Born |
-0.00003 |
0.2 |
0.00479 |
-28.4 |
-0.00015 |
0.9 |
| Shares Climate Info |
0.00003 |
-0.2 |
-0.01501 |
88.9 |
-0.00009 |
0.5 |
| Marital: Div/Widowed |
0.00002 |
-0.1 |
0.00661 |
-39.1 |
0.00008 |
-0.5 |
| Can Confide |
0.00001 |
-0.1 |
-0.00820 |
48.5 |
-0.00002 |
0.1 |
Step 9 — Correlation
Matrix
cont_cols <- df %>%
select(life_satisfaction, happiness_today, stress_level,
loneliness_frequency, social_anxiety,
community_belonging, neighbor_trust,
family_support, friend_support, perceived_cohesion,
sc_index, daily_screen_time, number_of_platforms,
optimism, gratitude, sense_of_purpose)
corrplot(cor(cont_cols, use="pairwise.complete.obs"),
method="color", type="upper", order="hclust",
addCoef.col="black", number.cex=0.6,
tl.cex=0.75, tl.col="black",
col=colorRampPalette(c("#C0392B","white","#2980B9"))(200),
title="Figure 5. Correlation Matrix", mar=c(0,0,2,0))

Step 10 — Well-being
by Region and Age
df %>%
filter(!is.na(region), !is.na(age_grp)) %>%
group_by(region, age_grp) %>%
summarise(pct=mean(high_wb, na.rm=TRUE)*100, .groups="drop") %>%
ggplot(aes(x=region, y=pct, fill=age_grp)) +
geom_col(position=position_dodge(0.8), width=0.7) +
scale_fill_brewer(palette="Set2", name="Age") +
scale_y_continuous(limits=c(0,100), labels=function(x) paste0(x,"%")) +
labs(title="Figure 6. High Well-being by Region and Age Group",
x="Region", y="Prevalence (%)") +
theme_minimal(base_size=12) +
theme(legend.position="top")

Session Info
sessionInfo()
## R version 4.3.2 (2023-10-31)
## Platform: x86_64-apple-darwin20 (64-bit)
## Running under: macOS Ventura 13.2.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/Toronto
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] corrplot_0.95 scales_1.4.0 patchwork_1.3.0 broom_1.0.8
## [5] rio_1.2.3 knitr_1.50 lubridate_1.9.4 forcats_1.0.0
## [9] stringr_1.5.1 dplyr_1.1.4 purrr_1.1.0 readr_2.1.5
## [13] tidyr_1.3.1 tibble_3.3.0 ggplot2_3.5.2 tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] sass_0.4.10 generics_0.1.4 xml2_1.3.8 stringi_1.8.7
## [5] hms_1.1.3 digest_0.6.37 magrittr_2.0.3 evaluate_1.0.4
## [9] grid_4.3.2 timechange_0.3.0 RColorBrewer_1.1-3 fastmap_1.2.0
## [13] R.oo_1.27.0 jsonlite_2.0.0 R.utils_2.12.3 backports_1.5.0
## [17] viridisLite_0.4.2 textshaping_1.0.1 jquerylib_0.1.4 cli_3.6.5
## [21] rlang_1.1.6 R.methodsS3_1.8.2 withr_3.0.2 cachem_1.1.0
## [25] yaml_2.3.10 tools_4.3.2 tzdb_0.5.0 kableExtra_1.4.0
## [29] curl_6.4.0 vctrs_0.6.5 R6_2.6.1 lifecycle_1.0.4
## [33] MASS_7.3-60 pkgconfig_2.0.3 pillar_1.11.0 bslib_0.9.0
## [37] gtable_0.3.6 data.table_1.17.8 glue_1.8.0 systemfonts_1.2.3
## [41] xfun_0.52 tidyselect_1.2.1 rstudioapi_0.17.1 dichromat_2.0-0.1
## [45] farver_2.1.2 htmltools_0.5.8.1 svglite_2.2.1 labeling_0.4.3
## [49] rmarkdown_2.29 compiler_4.3.2