#Load packages
library(foreign)
library(likert)
library(ggplot2)
library(usmap)
library(tibble)
library(tidyr)
library(plyr)
library(dplyr)
library(survey)
library(emmeans)
library(scales) # for percentage formatting
library(reshape2)
This resulted in a total of 1,017 completed surveys.
#Read in data
## [1] 1017
For this report, we are particularly interested in those participants who (1) report that they own or currently have access to a firearm, and (2) report their current storage methods. After subsetting for participants that meet these criteria, there was a total of 363 surveys used in this analysis.
#Comparison is always between those that practice safe storage (case/safe/lockbox) and those that do not
# In order to get this population of interest, we first subsetted data to only include those that respond "Yes" to "Do you currently own or have access to a firearm". This was question "BMW1"
data_owners=subset(data, BMW1 == "Yes") #this totals 363 participants
nrow(data_owners)
## [1] 363
The data also has a weighting value assigned to it. The variable “Weights” contains the proportional weighted value for each individual participant. This value will need to be applied to each measure variable evaluated below.
There is a “Skipped” option for each question asked, which is recorded if the individual did not respond. This will need to be accounted for in each question as well.
#Question BMW3 is "How often do you carry a loaded firearm".
BMW3.only= data_owners %>%
#Keep only the ID, Question/Responses for this question, Demographic information, and weighting score
dplyr::select(1, 17, 15,69,47:65) %>%
filter(BMW3 != "Skipped") %>% #filter out anyone who skipped the question
mutate(BMW5_2 = case_when(BMW5_2== "A safe, lockbox, or gun case (with a key, access code, or biometric lock)" ~ "1", TRUE ~ BMW5_2))
#Make columns with 0/1 binary data numerical variables in dataframe
BMW3.only$BMW5_2=as.numeric(BMW3.only$BMW5_2)
#Recreate categorical variables within this analysis
BMW3.only$ppkid017 <- ifelse(BMW3.only$ppkid017 > 0, "yes", ifelse(BMW3.only$ppkid017 == 0, "no", BMW3.only$ppkid017))
BMW3.only$age.cat <- cut(BMW3.only$ppage,
breaks = c(18, 25, 40, 60, Inf),
labels = c("18-25", "26-40", "41-60", ">61"),
right = FALSE)
#get the number of participants that chose each response for graphing purposes
BMW3.only %>%
dplyr::count(BMW3, BMW5_2) %>%
tidyr::spread(key = BMW5_2, value = n, fill = 0)
## BMW3 0 1 <NA>
## 1 Daily 14 23 0
## 2 At least once a week, but not every day 6 22 1
## 3 At least once a month, but not every week 6 13 0
## 4 At least once a year, but not every month 6 9 0
## 5 Almost never 37 51 2
## 6 I own a firearm but never carry it loaded 75 90 1
# Group data by BMW3, calculate the weighted mean of BMW5_2 within each group using the survey weights, keeps one row per group, and outputs a summary table with the group label (response options) renamed as parameter.
chart=BMW3.only %>%
group_by(BMW3) %>%
dplyr::summarize(weighted_mean_BMW5_2 = weighted.mean(BMW5_2, Weights, na.rm = TRUE)) %>%
ungroup() %>%
filter(BMW3 != "Skipped")
chart
## # A tibble: 6 × 2
## BMW3 weighted_mean_BMW5_2
## <fct> <dbl>
## 1 Daily 0.640
## 2 At least once a week, but not every day 0.774
## 3 At least once a month, but not every week 0.681
## 4 At least once a year, but not every month 0.617
## 5 Almost never 0.586
## 6 I own a firearm but never carry it loaded 0.540
#Rename categoires for graphing
chart <- chart %>%
mutate(BMW3 = case_when(
BMW3 == "I own a firearm but never carry it loaded" ~ "Never",
BMW3 == "At least once a year, but not every month" ~ "Annually",
BMW3 == "At least once a month, but not every week" ~ "Monthly",
BMW3 == "At least once a week, but not every day" ~ "Weekly",
TRUE ~ BMW3 # Keeps all other values as they are
))
#order levels for graphing
response_levels = c("Never", "Almost never", "Annually", "Monthly", "Weekly", "Daily")
chart$BMW3 <- factor(chart$BMW3, levels = response_levels)
#create plot showing frequency of carry among those with access to a firearm
ggplot(chart, aes(x = BMW3, y = weighted_mean_BMW5_2)) +
geom_line(group=1) +
geom_point() +
geom_label(aes(label = sprintf("%.3f", weighted_mean_BMW5_2)), nudge_y = 0.005, check_overlap = TRUE,
color = "black", size = 3, fontface = "bold",
label.padding = unit(0.2, "lines"),
label.size = 0.2) +
labs(x = "BMW3", y = "Weighted Mean BMW5_2", title = "Line Plot of BMW3 vs Weighted Mean BMW5_2") +
theme_minimal() +
xlab("Frequency of Carry") +
ggtitle("Proportion of Owners Using a Secure Container by Frequency of Carry") +
ylab("Proportion Using Secure Container")+
theme(axis.text.x = element_text(angle = 45, hjust = 1),
axis.title.x = element_text(size = 10, face = "bold"),
axis.title.y = element_text(size = 10, face = "bold"),
plot.title = element_text(size = 12, face = "bold", hjust = 0.5))
# Recode BMW3 levels for readability
BMW3.only <- BMW3.only %>%
mutate(BMW3 = case_when(
BMW3 == "I own a firearm but never carry it loaded" ~ "Never (47%)",
BMW3 == "At least once a year, but not every month" ~ "Annually (4%)",
BMW3 == "At least once a month, but not every week" ~ "Monthly (5%)",
BMW3 == "At least once a week, but not every day" ~ "Weekly (7%)",
BMW3 == "Almost every day" ~ "Daily (11%)",
BMW3 == "Almost never" ~ "Almost never (26%)",
BMW3 == "Daily" ~ "Daily (11%)",
TRUE ~ BMW3
))
response_levels <- c(
"Never (47%)", "Almost never (26%)", "Annually (4%)",
"Monthly (5%)", "Weekly (7%)", "Daily (11%)"
)
BMW3.only$BMW3 <- factor(BMW3.only$BMW3, levels = response_levels)
# NEW: remove anything not in those levels (prevents NA categories)
BMW3.only <- BMW3.only %>% dplyr::filter(!is.na(BMW3))
chart <- BMW3.only %>%
dplyr::group_by(BMW3) %>%
dplyr::summarize(
weighted_mean = weighted.mean(BMW5_2, Weights, na.rm = TRUE),
n = dplyr::n(), # CHANGED
total_n = nrow(BMW3.only),
.groups = "drop"
) %>%
dplyr::mutate(prop_n = n / total_n)
# Plot with bubble sizes representing proportion of sample
ggsave(
filename = "Figure 1.png",
dpi = 300,
width = 7,
height = 5,
units = "in",
plot =
ggplot(chart, aes(x = BMW3, y = weighted_mean, group = 1)) +
geom_line(color = "#3F326C", size = 1) +
geom_point(aes(size = prop_n), color = "#5C7750", alpha = 0.7) +
geom_label(aes(label = scales::percent(weighted_mean, accuracy = 0.1)),
nudge_y = 0.02,
size = 3,
fontface = "bold",
label.padding = unit(0.2, "lines"),
label.size = 0.2) +
scale_size_continuous(range = c(3, 10), name = "Sample Proportion") +
scale_y_continuous(labels = scales::percent_format(accuracy = 1)) +
labs(
x = "Frequency of Carry",
y = "Proportion Using a Secure Container",
title = "Proportion of Owners Using a Secure Container by Frequency of Carry"
) +
theme_bw() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
axis.title = element_text(size = 10, face = "bold"),
plot.title = element_text(size = 12, face = "bold", hjust = 0.5),
legend.position = "none"
))
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
bubble_values <- chart %>%
dplyr::select(BMW3, weighted_mean, n, total_n, prop_n) %>%
dplyr::arrange(BMW3)
print(bubble_values)
## # A tibble: 6 × 5
## BMW3 weighted_mean n total_n prop_n
## <fct> <dbl> <int> <int> <dbl>
## 1 Never (47%) 0.540 166 356 0.466
## 2 Almost never (26%) 0.586 90 356 0.253
## 3 Annually (4%) 0.617 15 356 0.0421
## 4 Monthly (5%) 0.681 19 356 0.0534
## 5 Weekly (7%) 0.774 29 356 0.0815
## 6 Daily (11%) 0.640 37 356 0.104
bubble_values %>%
dplyr::mutate(
weighted_mean_label = scales::percent(weighted_mean, accuracy = 0.1),
prop_n_label = scales::percent(prop_n, accuracy = 0.1)
) %>%
dplyr::select(BMW3, weighted_mean_label, n, prop_n_label) %>%
print()
## # A tibble: 6 × 4
## BMW3 weighted_mean_label n prop_n_label
## <fct> <chr> <int> <chr>
## 1 Never (47%) 54.0% 166 46.6%
## 2 Almost never (26%) 58.6% 90 25.3%
## 3 Annually (4%) 61.7% 15 4.2%
## 4 Monthly (5%) 68.1% 19 5.3%
## 5 Weekly (7%) 77.4% 29 8.1%
## 6 Daily (11%) 64.0% 37 10.4%
#All findings are subsetted by whether or not participants use secure storage, so first, BMW5 is process to get those populations
#Question BMW5 is "Do you use any of the following measures to safely store your firearms" and BMW5_2 specifies responses that do report the use of "A safe, lockbox, or gun case". Anyone who did not report the use of these tools has a value of "0". Those that do use these measures for safety have "A safe, lockbox, or gun case" in the cell.
BMW5.only= data_owners %>%
#Keep only the ID, Question/Responses for this question, Demographic information, and weighting score
select(c(1, 69, 16:25, 47:65)) %>%
#filter out anyone who skipped the question
filter(BMW5_1 != "Skipped") %>% #this drops the sample size to 355 from 363
#replace all text responses with a "1" value to allow adjusting by weight
mutate(BMW5_1 = case_when(BMW5_1== "Keeping all firearms unloaded" ~ "1", TRUE ~ BMW5_1)) %>%
mutate(BMW5_2 = case_when(BMW5_2== "A safe, lockbox, or gun case (with a key, access code, or biometric lock)" ~ "1", TRUE ~ BMW5_2)) %>%
mutate(BMW5_3 = case_when(BMW5_3== "A cable or trigger lock" ~ "1", TRUE ~ BMW5_3)) %>%
mutate(BMW5_4 = case_when(BMW5_4== "Motion alarm or notifications for firearm access" ~ "1", TRUE ~ BMW5_4)) %>%
mutate(BMW5_5 = case_when(BMW5_5== "Firearm sensor for close range or self-aiming" ~ "1", TRUE ~ BMW5_5)) %>%
mutate(BMW5_6 = case_when(BMW5_6== "Locking up ammunition separate from firearms" ~ "1", TRUE ~ BMW5_6)) %>%
mutate(BMW5_7 = case_when(BMW5_7== "Disassembling firearms and storing parts separately" ~ "1", TRUE ~ BMW5_7)) %>%
mutate(BMW5_8 = case_when(BMW5_8== "Entrusting keys or firearm parts to a trusted person or organization" ~ "1", TRUE ~ BMW5_8)) %>%
mutate(BMW5_9 = case_when(BMW5_9== "Using other safety measure not listed here (please specify)" ~ "1", TRUE ~ BMW5_9)) %>%
mutate(BMW5_10 = case_when(BMW5_10== "None of the above" ~ "1", TRUE ~ BMW5_10))
#Make columns with 0/1 binary data numerical variables in dataframe
for (i in 3:12) {
BMW5.only[[i]] <- as.numeric(BMW5.only[[i]])
}
#BMW5_2 is the lockbox/safe/case. Create a column that is yes/no to the use of safe/lockbox/case
BMW5.only$`Lock.Safe` <- ifelse(BMW5.only$BMW5_2 > 0, "yes", "no")
#Create categorical yes/no responses for kids present in home at over and under 18 years of age to use in analysis. Respondents reported the number of children in the home. We chose to use yes/no designation here.
BMW5.only$ppkid017.cat <- ifelse(BMW5.only$ppkid017 > 0, "yes", ifelse(BMW5.only$ppkid017 == 0, "no", BMW5.only$ppkid017))
BMW5.only$ppt18ov.cat <- ifelse(BMW5.only$ppt18ov > 0, "yes", ifelse(BMW5.only$ppt18ov == 0, "no", BMW5.only$ppt18ov))
#Create categorical variables for age within the dataset.
BMW5.only$age.cat <- cut(BMW5.only$ppage,
breaks = c(18, 25, 40, 60, Inf),
labels = c("1. 18-25", "2. 26-40", "3. 41-60", "4. >61"),
right = FALSE)
library(reshape2)
#Question BMW5 is "Do you use any of the following measures to safely store your firearms"
#BMW5_1 contains data for "Keeping all firearms unloaded"
#MBW5_2 A safe, lockbox, or gun case
#BMW5_3 contains data for "A cable or trigger lock"
#BMW4_4 contains data for "Motion alarm or notifications for firearm access"
#BMW5_5 contains data for "Firearm sensor for close range or self-aiming"
#BMW5_6 contains data for "Locking up ammunition separate from firearms"
#BMW5_7 contains data for "Disassembling firearms and storing parts separately"
#BMW5_8 contains data for "Entrusting keys or firearm parts to a trusted person or organization"
#BMW5_9 contains data for "Using other safety measures not listed here (please specify)"
#BMW5_10 contains data for "None of the above"
#BMW5_9_TEXT contains self reported methods of storage by those who selected "other"
item_labels <- tibble::tribble(
~item, ~measure,
"BMW5_1", "Keeping all firearms unloaded",
"BMW5_3", "Using a cable or trigger lock",
"BMW5_4", "Using motion alarm or access notifications",
"BMW5_5", "Firearm sensor for close-range or self-aiming",
"BMW5_6", "Locking up ammunition separate from firearms",
"BMW5_7", "Disassembling firearms and storing parts separately",
"BMW5_8", "Entrusting keys or parts to a trusted person or organization",
"BMW5_9", "Using other safety measures not listed here",
"BMW5_10", "None of the above"
)
#all users
BMW5_all=BMW5.only
sum(BMW5_all$Weights) #333.9897
## [1] 333.9897
#get sum of weights in each column. first multiply by 0, or 1. This will replace a weight with 0 for an individual who said no. And sum the individuals who said yes.
sumBMW5all=BMW5_all %>%
dplyr::summarise(across(starts_with("BMW5_"),
~ sum(.x * Weights)))
#convert to proportions
percentBMW5all <- 100 * sumBMW5all / sum(BMW5_all$Weights)
percentBMW5all
## BMW5_1 BMW5_2 BMW5_3 BMW5_4 BMW5_5 BMW5_6 BMW5_7 BMW5_8
## 1 51.19077 59.06157 22.06666 2.760355 1.148505 38.13443 6.562758 5.701756
## BMW5_9 BMW5_10
## 1 3.138727 14.27945
#Only those who do not use a lockbox/gun safe
BMW5_secure= subset(BMW5.only, Lock.Safe == "no")
sum(BMW5_secure$Weights) #136.7302
## [1] 136.7302
sumBMW5sec=BMW5_secure %>%
dplyr::summarise(across(starts_with("BMW5_"),
~ sum(.x * Weights)))
percentBMW5sec <- 100 * sumBMW5sec / sum(BMW5_secure$Weights)
percentBMW5sec
## BMW5_1 BMW5_2 BMW5_3 BMW5_4 BMW5_5 BMW5_6 BMW5_7 BMW5_8
## 1 44.55068 0 17.2489 0.5774933 0.8490075 27.12474 5.824486 1.966883
## BMW5_9 BMW5_10
## 1 4.581305 34.8803
#Question BMW6 is "Which of the following methods for storing firearms would you consider using"
#BMW6_1 contains data for "Keeping all firearms unloaded"
#BMW6_3 contains data for "A cable or trigger lock"
#BMW6_4 contains data for "Motion alarm or notifications for firearm access"
#BMW6_5 contains data for "Firearm sensor for close range or self-aiming"
#BMW6_6 contains data for "Locking up ammunition separate from firearms"
#BMW6_7 contains data for "Disassembling firearms and storing parts separately"
#BMW6_8 contains data for "Entrusting keys or firearm parts to a trusted person or organization"
#BMW6_9 contains data for "Using other safety measures not listed here (please specify)"
#BMW6_10 contains data for "None of the above"
#BMW6_9_TEXT contains self reported methods of storage by those who selected "other"
BMW6.only= data_owners %>%
dplyr::select(c(1,17, 16:25, 27:36, 47:65, 69)) %>%
filter(BMW6_1 != "Skipped" | is.na(BMW6_1)) #filter skipped by keep NA values
BMW6.only= BMW6.only %>%
mutate(BMW5_2 = case_when(BMW5_2== "A safe, lockbox, or gun case (with a key, access code, or biometric lock)" ~ "1", TRUE ~ BMW5_2)) %>%
mutate(BMW6_1 = case_when(BMW6_1== "Keeping all firearms unloaded" ~ "1", TRUE ~ BMW6_1)) %>%
mutate(BMW6_2 = case_when(BMW6_2== "A safe, lockbox, or gun case (with a key, access code, or biometric lock)" ~ "1", TRUE ~ BMW6_2)) %>%
mutate(BMW6_3 = case_when(BMW6_3== "A cable or trigger lock" ~ "1", TRUE ~ BMW6_3)) %>%
mutate(BMW6_4 = case_when(BMW6_4== "Motion alarm or notifications for firearm access" ~ "1", TRUE ~ BMW6_4)) %>%
mutate(BMW6_5 = case_when(BMW6_5== "Firearm sensor for close range or self-aiming" ~ "1", TRUE ~ BMW6_5)) %>%
mutate(BMW6_6 = case_when(BMW6_6== "Locking up ammo separate from firearms" ~ "1", TRUE ~ BMW6_6)) %>%
mutate(BMW6_7 = case_when(BMW6_7== "Disassembling firearms and storing parts separately" ~ "1", TRUE ~ BMW6_7)) %>%
mutate(BMW6_8 = case_when(BMW6_8== "Entrusting keys or firearm parts to a trusted person or organization" ~ "1", TRUE ~ BMW6_8)) %>%
mutate(BMW6_9 = case_when(BMW6_9== "Using other safety measure not listed here (please specify)" ~ "1", TRUE ~ BMW6_9)) %>%
mutate(BMW6_10 = case_when(BMW6_10== "None of the above" ~ "1", TRUE ~ BMW6_10))
#Make columns with 0/1 binary data numerical variables in dataframe
for (i in 2:21) {
BMW6.only[[i]] <- as.numeric(BMW6.only[[i]])
}
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
BMW6.only$ppkid017 <- ifelse(BMW6.only$ppkid017 > 0, "yes", ifelse(BMW6.only$ppkid017 == 0, "no", BMW6.only$ppkid017))
BMW6.only$age.cat <- cut(BMW6.only$ppage,
breaks = c(18, 25, 40, 60, Inf),
labels = c("18-25", "26-40", "41-60", ">61"),
right = FALSE)
BMW6.only$`Lock.Safe` <- ifelse(BMW6.only$BMW5_2 > 0, "yes", "no")
#because of survey logic, if they said "yes" to an option in BMW5 they were not shown it in BMW6. So we are replacing NA with "0". As they are not potential future adopters.
BMW6.only <- BMW6.only %>%
mutate(across(starts_with("BMW6_"),
~ replace_na(.x, 0)))
#all users
BMW6_all=BMW6.only
sum(BMW6_all$Weights) #333.9897
## [1] 339.6666
#get sum of weights in each column. first multiply by 0, or 1. This will replace a weight with 0 for an individual who said no. And sum the individuals who said yes.
sumBMW6all=BMW6_all %>%
dplyr::summarise(across(starts_with("BMW6_"),
~ sum(.x * Weights)))
#convert to proportions
percentBMW6all <- 100 * sumBMW6all / sum(BMW6_all$Weights)
percentBMW6all
## BMW6_1 BMW6_2 BMW6_3 BMW6_4 BMW6_5 BMW6_6 BMW6_7 BMW6_8
## 1 12.05202 21.09652 19.83761 18.82692 8.728971 14.21035 7.125905 4.844154
## BMW6_9 BMW6_10
## 1 3.734571 35.38603
#Only those who do not use a lockbox/gun safe
BMW6_secure= subset(BMW6.only, Lock.Safe == "no")
sum(BMW6_secure$Weights) #136.7302
## [1] 136.7302
sumBMW6sec=BMW6_secure %>%
dplyr::summarise(across(starts_with("BMW6_"),
~ sum(.x * Weights)))
percentBMW6sec <- 100 * sumBMW6sec / sum(BMW6_secure$Weights)
percentBMW6sec
## BMW6_1 BMW6_2 BMW6_3 BMW6_4 BMW6_5 BMW6_6 BMW6_7 BMW6_8
## 1 15.03943 50.35738 12.27476 11.45228 5.482342 15.78 3.552276 2.588313
## BMW6_9 BMW6_10
## 1 4.117316 34.87712
We will remove anyone who said “strongly agree” or “somewhat agree” to option 7, because we are assuming that it should have been read as mutually exclusive. So we are looking for motivators of change in people who stated that they did not agree they would always keep at least one unlocked.
#Question BMW4 is "how much do you agree or disagree with each of the following". A series of potential motivators for gun safety are then presented.
#BMW4_1 contains data for "If I had kids at home I would lock up my firearms"
#BMW4_2 contains data for "If someone in my home was struggling with mental or physical health, I would lock up my firearm(s)."
#BMW4_3 contains data for "If a close friend or family asked me to, I would lock up my firearm(s).
#BMW4_4 contains data for "To prevent my firearm(s) from being used, stolen, or damaged, I would lock it up."
#BMW4_5 contains data for "To prevent accidental injury from my firearm(s), I would lock it up."
#BMW4_6 contains data for "To prevent suicide for me or others, I would lock up my firearm(s)."
#BMW4_7 contains data for "I plan to always keep at least one firearm unlocked."
BMW4.only= data_owners %>%
#Keep only the ID, Question/Responses for this question, Demographic information, and weighting score
dplyr::select(1, 17, 38:44,69,47:65) %>%
#filter out anyone who skipped the question
mutate(BMW5_2 = case_when(BMW5_2== "A safe, lockbox, or gun case (with a key, access code, or biometric lock)" ~ "1", TRUE ~ BMW5_2))
#Make columns with 0/1 binary data numerical variables in dataframe
BMW4.only$BMW5_2=as.numeric(BMW4.only$BMW5_2)
## Warning: NAs introduced by coercion
BMW4.only$ppkid017 <- ifelse(BMW4.only$ppkid017 > 0, "yes", ifelse(BMW4.only$ppkid017 == 0, "no", BMW4.only$ppkid017))
BMW4.only$age.cat <- cut(BMW4.only$ppage,
breaks = c(18, 25, 40, 60, Inf),
labels = c("18-25", "26-40", "41-60", ">61"),
right = FALSE)
#Subset data to be those who disagree and those who agree with option 7 for planning to always keeping one unlocked
BMW4_7T <- BMW4.only %>%
filter(BMW4_7 %in% c("Strongly agree", "Somewhat agree"))
likert_long <- BMW4_7T %>%
select(starts_with("BMW4_"), -BMW4_7, Weights) %>%
pivot_longer(
cols = starts_with("BMW4_"),
names_to = "Question",
values_to = "Response"
)
plot_df <- likert_long %>%
mutate(
strong = Response == "Strongly agree",
somewhat = Response == "Somewhat agree"
) %>%
group_by(Question) %>%
summarise(
strong_pct = 100 * sum(Weights * strong, na.rm = TRUE) / sum(Weights, na.rm = TRUE),
top2_pct = 100 * sum(Weights * (strong | somewhat), na.rm = TRUE) / sum(Weights, na.rm = TRUE)
) %>%
ungroup()
plot_df <- plot_df %>%
mutate(
p = top2_pct / 100,
n_eff = sum(BMW4_7T$Weights)^2 / sum(BMW4_7T$Weights^2), # Kish effective N
se = sqrt(p * (1 - p) / n_eff),
low = 100 * (p - 1.96 * se),
high = 100 * (p + 1.96 * se)
)
plot_df <- plot_df %>%
mutate(Question = case_when(
Question == "BMW4_1" ~ "If I had kids at home...",
Question == "BMW4_2" ~ "If someone in my home was struggling with mental or physical health...",
Question == "BMW4_3" ~ "If a close friend or family asked me to...",
Question == "BMW4_4" ~ "To prevent my firearm(s) from being used, stolen, or damaged...",
Question == "BMW4_5" ~ "To prevent accidental injury from my firearm(s)...",
Question == "BMW4_6" ~ "To prevent suicide for me or others...",
TRUE ~ Question
))
motiv <- ggplot(plot_df, aes(y = reorder(Question, top2_pct))) +
# Top 2 (dark red)
geom_col(aes(x = top2_pct), fill = "#7E1417", width = 0.7) +
# Strongly agree (dark blue overlay)
geom_col(aes(x = strong_pct), fill = "#001042", width = 0.7) +
geom_errorbarh(aes(xmin = low, xmax = high), height = 0.2) +
geom_text(
aes(x = -3, label = sprintf("%.1f%%", top2_pct)),
hjust = 1,
size = 4
) +
scale_x_continuous(
limits = c(-8, max(plot_df$high) + 8)
) +
labs(
title = "Agreement with Motivators Among Those Who Will Keep One Unlocked",
subtitle = "Proportion of respondents who
<span style='color:#001042; font-weight:bold;'>Somewhat Agree</span>
or
<span style='color:#7E1417; font-weight:bold;'>Strongly Agree</span>
<br>(Survey-weighted estimates with 95% confidence intervals)",
y = NULL,
x = "Percent"
) +
theme_minimal(base_size = 14) +
theme(
plot.background = element_rect(fill = "white", color = NA),
panel.background = element_rect(fill = "white", color = NA),
plot.title = element_text(size = 20, face = "bold", hjust = 0.5),
plot.subtitle = ggtext::element_markdown(size = 15, hjust = 0.5),
axis.text.y = element_text(size = 14),
axis.text.x = element_text(size = 14),
axis.title.x = element_text(size = 15),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank()
)
## Warning: `geom_errorbarh()` was deprecated in ggplot2 4.0.0.
## ℹ Please use the `orientation` argument of `geom_errorbar()` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
BMW4_7F <- BMW4.only %>%
filter(BMW4_7 %in% c("Strongly disagree", "Somewhat disagree"))
likert_long2 <- BMW4_7F %>%
select(starts_with("BMW4_"), -BMW4_7, Weights) %>%
pivot_longer(
cols = starts_with("BMW4_"),
names_to = "Question",
values_to = "Response"
)
plot_df2 <- likert_long2 %>%
mutate(
strong = Response == "Strongly agree",
somewhat = Response == "Somewhat agree"
) %>%
group_by(Question) %>%
summarise(
strong_pct = 100 * sum(Weights * strong, na.rm = TRUE) / sum(Weights, na.rm = TRUE),
top2_pct = 100 * sum(Weights * (strong | somewhat), na.rm = TRUE) / sum(Weights, na.rm = TRUE)
) %>%
ungroup()
plot_df2 <- plot_df2 %>%
mutate(
p = top2_pct / 100,
n_eff = sum(BMW4_7F$Weights)^2 / sum(BMW4_7F$Weights^2), # Kish effective N
se = sqrt(p * (1 - p) / n_eff),
low = 100 * (p - 1.96 * se),
high = 100 * (p + 1.96 * se)
)
plot_df2 <- plot_df2 %>%
mutate(Question = case_when(
Question == "BMW4_1" ~ "If I had kids at home...",
Question == "BMW4_2" ~ "If someone in my home was struggling with mental or physical health...",
Question == "BMW4_3" ~ "If a close friend or family asked me to...",
Question == "BMW4_4" ~ "To prevent my firearm(s) from being used, stolen, or damaged...",
Question == "BMW4_5" ~ "To prevent accidental injury from my firearm(s)...",
Question == "BMW4_6" ~ "To prevent suicide for me or others...",
TRUE ~ Question
))
motiv2 <- ggplot(plot_df2, aes(y = reorder(Question, top2_pct))) +
# Top 2 (dark red)
geom_col(aes(x = top2_pct), fill = "#7E1417", width = 0.7) +
# Strongly agree (dark blue overlay)
geom_col(aes(x = strong_pct), fill = "#001042", width = 0.7) +
geom_errorbarh(aes(xmin = low, xmax = high), height = 0.2) +
geom_text(
aes(x = -3, label = sprintf("%.1f%%", top2_pct)),
hjust = 1,
size = 4
) +
scale_x_continuous(
limits = c(-8, max(plot_df$high) + 8)
) +
labs(
title = "Agreement with Motivators Among Those Who Will Not Keep One Unlocked",
subtitle = "Proportion of respondents who
<span style='color:#001042; font-weight:bold;'>Somewhat Agree</span>
or
<span style='color:#7E1417; font-weight:bold;'>Strongly Agree</span>
<br>(Survey-weighted estimates with 95% confidence intervals)",
y = NULL,
x = "Percent"
) +
theme_minimal(base_size = 14) +
theme(
plot.background = element_rect(fill = "white", color = NA),
panel.background = element_rect(fill = "white", color = NA),
plot.title = element_text(size = 20, face = "bold", hjust = 0.5),
plot.subtitle = ggtext::element_markdown(size = 15, hjust = 0.5),
axis.text.y = element_text(size = 14),
axis.text.x = element_text(size = 14),
axis.title.x = element_text(size = 15),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank()
)
motiv2 <- motiv2 +
labs(y = NULL) +
theme(
axis.text.y = element_blank(),
plot.title = element_blank(),
plot.subtitle = element_blank(),
)
library(patchwork)
two_panel <- motiv + motiv2 +
plot_layout(ncol = 2)
two_panel
## `height` was translated to `width`.
## `height` was translated to `width`.
ggsave(
"motiv_two_panel.png",
two_panel,
width = 16,
height = 8,
dpi = 300
)
## `height` was translated to `width`.
## `height` was translated to `width`.
ggsave(
"motiv.png",
motiv,
width = 12,
height = 7,
dpi = 600
)
## `height` was translated to `width`.
ggsave(
"motiv_two.png",
motiv2,
width = 12,
height = 7,
dpi = 600
)
## `height` was translated to `width`.
Weighted percentages of respondents reporting the use of a safe, lockbox, or gun case were calculated using survey weights to account for sampling design. Weighted means and their 95% confidence intervals were estimated using the survey package in R, which incorporates respondent-level weights. Confidence intervals were derived using standard errors obtained from the weighted survey design, providing estimates that reflect the population distribution of the sampled respondents.
#This function computes survey-weighted outcome percentages with standard errors and confidence intervals by each demographic factor, then fits a survey-weighted logistic regression of the outcome (odds of secure container use) on that predictor and returns both results.
survey_summary <- function(var_name, design, outcome = "BMW5_2") {
# Weighted percentages with SE and CI
ci_table <- svyby(
formula = as.formula(paste0("~", outcome)),
by = as.formula(paste0("~", var_name)),
design = design,
FUN = svymean,
vartype = c("se", "ci"),
na.rm = TRUE
) %>%
as.data.frame() %>%
rename_with(~ c(var_name, "Weighted_Percentage", "SE", "CI_Lower", "CI_Upper"), everything()) %>%
mutate(
Weighted_Percentage = Weighted_Percentage * 100,
CI_Lower = CI_Lower * 100,
CI_Upper = CI_Upper * 100
)
# Survey-weighted logistic regression
formula_str <- paste0(outcome, " ~ ", var_name)
fit <- svyglm(as.formula(formula_str), design = design, family = quasibinomial())
list(
CI_Table = ci_table,
Regression = list(
Summary = summary(fit),
CI = confint(fit)
)
)
}
# Create survey design object once
design <- svydesign(ids = ~1, data = BMW5.only, weights = ~Weights)
# List of variables for which we will run a regression
vars <- c("ppgender", "age.cat", "xparty4", "xurbanicity", "ppeducat",
"ppeduc5", "ppethm", "pphouse4", "ppinc7", "ppmarit5",
"ppmsacat", "ppreg4", "ppemploy", "ppkid017.cat", "ppt18ov")
# Apply function to all variables
results_list <- lapply(vars, function(v) survey_summary(v, design))
# Access example:
# Weighted CI table for age
results_list$age.cat$CI_Table # won't work directly since lapply returns unnamed list
# Better to name the list elements:
names(results_list) <- vars
for (var in names(results_list)) {
cat("\n==============================\n")
cat("Variable:", var, "\n")
cat("==============================\n")
cat("\nWeighted Percentages + CI:\n")
print(results_list[[var]]$CI_Table)
cat("\nSurvey-weighted Logistic Regression:\n")
print(results_list[[var]]$Regression$Summary)
cat("\nConfidence Intervals for coefficients:\n")
print(results_list[[var]]$Regression$CI)
cat("\n\n") # extra space between variables
}
Weighted % (SE, CI)
| ppgender | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Male | 59.07602 | 0.03518634 | 52.17963 | 65.97242 |
| Female | 59.04026 | 0.04319632 | 50.57394 | 67.50658 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.44 | 0.37 | 0.15 | 0.08 | 0.65 | 0.012 |
| ppgenderFemale | 1.00 | 0.00 | 0.23 | -0.45 | 0.45 | 0.995 |
Weighted % (SE, CI)
| age.cat | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
|
73.65499 | 0.08509242 | 56.97718 | 90.33280 |
|
66.13982 | 0.05446808 | 55.46428 | 76.81537 |
|
60.11025 | 0.04339029 | 51.60591 | 68.61459 |
|
50.53462 | 0.04963460 | 40.80641 | 60.26282 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 2.80 | 1.03 | 0.44 | 0.17 | 1.89 | 0.020 |
| age.cat2. 26-40 | 0.70 | -0.36 | 0.50 | -1.34 | 0.63 | 0.475 |
| age.cat3. 41-60 | 0.54 | -0.62 | 0.47 | -1.55 | 0.31 | 0.193 |
| age.cat4. >61 | 0.37 | -1.01 | 0.48 | -1.95 | -0.06 | 0.037 |
Weighted % (SE, CI)
| xparty4 | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Republican | 61.58013 | 0.04287909 | 53.17599 | 69.98428 |
| Democrat | 56.71812 | 0.06896706 | 43.20082 | 70.23541 |
| Independent | 57.15847 | 0.04612307 | 48.11852 | 66.19843 |
| Something else | 59.65833 | 0.09011666 | 41.99579 | 77.32087 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.60 | 0.47 | 0.18 | 0.12 | 0.83 | 0.010 |
| xparty4Democrat | 0.82 | -0.20 | 0.33 | -0.86 | 0.46 | 0.547 |
| xparty4Independent | 0.83 | -0.18 | 0.26 | -0.70 | 0.33 | 0.483 |
| xparty4Something else | 0.92 | -0.08 | 0.42 | -0.90 | 0.74 | 0.847 |
Weighted % (SE, CI)
| xurbanicity | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Urban | 57.96007 | 0.05443195 | 47.29161 | 68.62854 |
| Rural | 58.72802 | 0.05132055 | 48.66938 | 68.78667 |
| Suburban | 59.88209 | 0.03995091 | 52.05185 | 67.71232 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.38 | 0.32 | 0.22 | -0.12 | 0.76 | 0.151 |
| xurbanicityRural | 1.03 | 0.03 | 0.31 | -0.57 | 0.64 | 0.918 |
| xurbanicitySuburban | 1.08 | 0.08 | 0.28 | -0.47 | 0.63 | 0.776 |
Weighted % (SE, CI)
| ppeducat | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Less than HS | 33.17008 | 0.11649244 | 10.33798 | 56.00218 |
| HS | 56.91554 | 0.05112485 | 46.89526 | 66.93583 |
| Some college | 56.18232 | 0.04972332 | 46.43673 | 65.92791 |
| Bachelor or higher | 68.49396 | 0.04210514 | 60.24151 | 76.74642 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 0.50 | -0.70 | 0.53 | -1.73 | 0.33 | 0.183 |
| ppeducatHS | 2.66 | 0.98 | 0.57 | -0.13 | 2.09 | 0.084 |
| ppeducatSome college | 2.58 | 0.95 | 0.56 | -0.16 | 2.06 | 0.093 |
| ppeducatBachelor or higher | 4.38 | 1.48 | 0.56 | 0.37 | 2.58 | 0.009 |
Weighted % (SE, CI)
| ppeduc5 | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| No high school diploma or GED | 33.17008 | 0.11649244 | 10.33798 | 56.00218 |
| High school graduate (high school diploma or the equivalent GED) | 56.91554 | 0.05112485 | 46.89526 | 66.93583 |
| Some college or Associate degree | 56.18232 | 0.04972332 | 46.43673 | 65.92791 |
| Bachelor’s degree | 64.11467 | 0.05686230 | 52.96987 | 75.25948 |
| Master’s degree or above | 74.44852 | 0.06225743 | 62.24629 | 86.65075 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 0.50 | -0.70 | 0.53 | -1.73 | 0.33 | 0.183 |
| ppeduc5High school graduate (high school diploma or the equivalent GED) | 2.66 | 0.98 | 0.57 | -0.13 | 2.09 | 0.084 |
| ppeduc5Some college or Associate degree | 2.58 | 0.95 | 0.56 | -0.16 | 2.06 | 0.093 |
| ppeduc5Bachelor’s degree | 3.60 | 1.28 | 0.58 | 0.14 | 2.42 | 0.028 |
| ppeduc5Master’s degree or above | 5.87 | 1.77 | 0.62 | 0.55 | 2.99 | 0.005 |
Weighted % (SE, CI)
| ppethm | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| White, Non-Hispanic | 58.37785 | 0.03004375 | 52.48939 | 64.26632 |
| Black or African American, Non-Hispanic | 53.98844 | 0.10690988 | 33.03449 | 74.94239 |
| Other, Non-Hispanic | 78.57190 | 0.13820299 | 51.48461 | 105.65919 |
| Hispanic | 63.04836 | 0.09353498 | 44.71584 | 81.38088 |
| 2+ races, Non-Hispanic | 46.41445 | 0.13193034 | 20.55658 | 72.27232 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.40 | 0.34 | 0.12 | 0.10 | 0.58 | 0.007 |
| ppethmBlack or African American, Non-Hispanic | 0.84 | -0.18 | 0.45 | -1.06 | 0.70 | 0.691 |
| ppethmOther, Non-Hispanic | 2.61 | 0.96 | 0.83 | -0.67 | 2.59 | 0.248 |
| ppethmHispanic | 1.22 | 0.20 | 0.42 | -0.63 | 1.02 | 0.641 |
| ppethm2+ races, Non-Hispanic | 0.62 | -0.48 | 0.54 | -1.55 | 0.59 | 0.377 |
Weighted % (SE, CI)
| pphouse4 | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| One-family house detached from any other house | 61.06382 | 0.02940491 | 55.30056 | 66.82707 |
| One-family condo or townhouse attached to other units | 56.08947 | 0.12023152 | 32.52452 | 79.65441 |
| Building with 2 or more apartments | 38.98875 | 0.10243937 | 18.91100 | 59.06649 |
| Other (mobile home, boat, RV, van, etc.) | 49.01316 | 0.14027631 | 21.51951 | 76.50681 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.57 | 0.45 | 0.12 | 0.21 | 0.69 | 0.000 |
| pphouse4One-family condo or townhouse attached to other units | 0.81 | -0.21 | 0.50 | -1.20 | 0.79 | 0.684 |
| pphouse4Building with 2 or more apartments | 0.41 | -0.90 | 0.45 | -1.78 | -0.02 | 0.046 |
| pphouse4Other (mobile home, boat, RV, van, etc.) | 0.61 | -0.49 | 0.57 | -1.62 | 0.64 | 0.395 |
Weighted % (SE, CI)
| ppinc7 | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Under $10,000 | 56.82471 | 0.19122233 | 19.34582 | 94.30360 |
| $10,000 to $24,999 | 40.15789 | 0.10239880 | 20.08810 | 60.22769 |
| $25,000 to $49,999 | 44.98968 | 0.07934691 | 29.43797 | 60.54139 |
| $50,000 to $74,999 | 52.18102 | 0.06580500 | 39.28348 | 65.07856 |
| $75,000 to $99,999 | 54.79359 | 0.07507078 | 40.07999 | 69.50719 |
| $100,000 to $149,999 | 67.81940 | 0.05472496 | 57.09350 | 78.54529 |
| $150,000 or more | 70.16699 | 0.04924848 | 60.51446 | 79.81951 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.32 | 0.27 | 0.78 | -1.26 | 1.81 | 0.725 |
| ppinc7$10,000 to $24,999 | 0.51 | -0.67 | 0.89 | -2.42 | 1.07 | 0.449 |
| ppinc7$25,000 to $49,999 | 0.62 | -0.48 | 0.84 | -2.13 | 1.18 | 0.573 |
| ppinc7$50,000 to $74,999 | 0.83 | -0.19 | 0.82 | -1.81 | 1.43 | 0.820 |
| ppinc7$75,000 to $99,999 | 0.92 | -0.08 | 0.84 | -1.73 | 1.56 | 0.922 |
| ppinc7$100,000 to $149,999 | 1.60 | 0.47 | 0.82 | -1.14 | 2.08 | 0.566 |
| ppinc7$150,000 or more | 1.79 | 0.58 | 0.81 | -1.02 | 2.18 | 0.476 |
Weighted % (SE, CI)
| ppmarit5 | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Now married | 63.06410 | 0.03413009 | 56.37472 | 69.75347 |
| Widowed | 35.22435 | 0.12035218 | 11.63576 | 58.81294 |
| Divorced | 56.40292 | 0.08177034 | 40.37623 | 72.42962 |
| Separated | 57.33557 | 0.22982424 | 12.29084 | 102.38029 |
| Never married | 54.91611 | 0.06018871 | 43.11934 | 66.71288 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.71 | 0.53 | 0.15 | 0.25 | 0.82 | 0.000 |
| ppmarit5Widowed | 0.32 | -1.14 | 0.55 | -2.22 | -0.07 | 0.037 |
| ppmarit5Divorced | 0.76 | -0.28 | 0.36 | -0.99 | 0.44 | 0.446 |
| ppmarit5Separated | 0.79 | -0.24 | 0.95 | -2.11 | 1.63 | 0.801 |
| ppmarit5Never married | 0.71 | -0.34 | 0.28 | -0.90 | 0.22 | 0.235 |
Weighted % (SE, CI)
| ppmsacat | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Non-metro area | 57.7626 | 0.06326162 | 45.36355 | 70.16165 |
| Metro area | 59.3854 | 0.03021909 | 53.46257 | 65.30824 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.37 | 0.31 | 0.26 | -0.2 | 0.82 | 0.228 |
| ppmsacatMetro area | 1.07 | 0.07 | 0.29 | -0.5 | 0.63 | 0.816 |
Weighted % (SE, CI)
| ppreg4 | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Northeast | 65.25129 | 0.07496860 | 50.55772 | 79.94487 |
| Midwest | 65.81647 | 0.05368496 | 55.29441 | 76.33853 |
| South | 54.03173 | 0.04101341 | 45.99325 | 62.07021 |
| West | 59.11143 | 0.06474228 | 46.42218 | 71.80069 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.88 | 0.63 | 0.33 | -0.02 | 1.28 | 0.058 |
| ppreg4Midwest | 1.03 | 0.03 | 0.41 | -0.78 | 0.83 | 0.951 |
| ppreg4South | 0.63 | -0.47 | 0.37 | -1.20 | 0.26 | 0.206 |
| ppreg4West | 0.77 | -0.26 | 0.43 | -1.10 | 0.58 | 0.539 |
Weighted % (SE, CI)
| ppemploy | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| Working full-time | 63.26282 | 0.03662169 | 56.08510 | 70.44054 |
| Working part-time | 66.40362 | 0.07817543 | 51.08152 | 81.72572 |
| Not working | 51.09609 | 0.04640956 | 41.99998 | 60.19220 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.72 | 0.54 | 0.16 | 0.23 | 0.85 | 0.001 |
| ppemployWorking part-time | 1.15 | 0.14 | 0.38 | -0.62 | 0.89 | 0.720 |
| ppemployNot working | 0.61 | -0.50 | 0.24 | -0.98 | -0.02 | 0.041 |
Weighted % (SE, CI)
| ppkid017.cat | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| no | 54.62022 | 0.03302592 | 48.14726 | 61.09318 |
| yes | 69.74103 | 0.04693341 | 60.54225 | 78.93981 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.20 | 0.19 | 0.13 | -0.08 | 0.45 | 0.165 |
| ppkid017.catyes | 1.91 | 0.65 | 0.26 | 0.14 | 1.16 | 0.013 |
Weighted % (SE, CI)
| ppt18ov | Weighted_Percentage | SE | CI_Lower | CI_Upper |
|---|---|---|---|---|
| 1 | 49.96925 | 0.06990993 | 36.26716 | 63.67135 |
| 2 | 61.10978 | 0.03448161 | 54.35151 | 67.86805 |
| 3 | 63.16869 | 0.07174878 | 49.10619 | 77.23120 |
| 4 | 52.85846 | 0.09927923 | 33.40009 | 72.31683 |
| 5 | 46.03779 | 0.31157239 | -15.02928 | 107.10485 |
| 6 | 100.00000 | 0.00000000 | 100.00000 | 100.00000 |
Logistic regression (OR)
| term | OR | Estimate | SE | CI_Lower | CI_Upper | p_value |
|---|---|---|---|---|---|---|
| (Intercept) | 1.23 | 0.21 | 0.31 | -0.41 | 0.83 | 0.504 |
| ppt18ov | 1.07 | 0.07 | 0.13 | -0.19 | 0.34 | 0.600 |
design <- svydesign(ids = ~1, data = BMW5.only, weights = ~Weights)
# Survey-weighted logistic regression
fit_age <- svyglm(BMW5_2 ~ age.cat, design = design, family = quasibinomial())
# Estimated marginal means (predicted probabilities) by age group
emm_age <- emmeans(fit_age, ~ age.cat, type = "response") # response scale = probabilities
emm_df <- as.data.frame(emm_age) %>%
transmute(
age.cat,
p = prob,
lower = asymp.LCL,
upper = asymp.UCL,
pct = 100 * p,
pct_low = 100 * lower,
pct_up = 100 * upper
)
pairs_age <- pairs(emm_age, adjust = "holm") # or "tukey" if you prefer
pairs_df <- as.data.frame(pairs_age)
pairs_df
## contrast odds.ratio SE df null z.ratio p.value
## (1. 18-25) / (2. 26-40) 1.431298 0.7177270 Inf 1 0.715 0.7841
## (1. 18-25) / (3. 41-60) 1.855310 0.8801432 Inf 1 1.303 0.5918
## (1. 18-25) / 4. >61 2.736631 1.3173605 Inf 1 2.091 0.2190
## (2. 26-40) / (3. 41-60) 1.296243 0.3929562 Inf 1 0.856 0.7841
## (2. 26-40) / 4. >61 1.911993 0.6003166 Inf 1 2.064 0.2190
## (3. 41-60) / 4. >61 1.475026 0.3962663 Inf 1 1.447 0.5918
##
## P value adjustment: holm method for 6 tests
## Tests are performed on the log odds ratio scale
p1 <- ggplot(emm_df, aes(x = age.cat, y = pct, group = 1)) +
geom_line() +
geom_point(size = 3) +
geom_errorbar(aes(ymin = pct_low, ymax = pct_up), width = 0.12) +
geom_text(
aes(label = sprintf("%.1f%%", pct)),
vjust = -1.1,
hjust= -0.3,
size = 3.5
) +
labs(
x = "Age category",
y = "Predicted probability of using a secure container (%)",
title = "Secure Container storage by age category",
subtitle = "Predicted probabilities from survey-weighted logistic regression (95% CI)"
) +
theme_classic()
p1
ggsave(
filename = "Supp Fig 1.png",
plot=p1,
dpi = 600,
width = 7,
height = 5,
units = "in")