1 Setup and Data Loading

1.1 Required Packages

# Install any missing packages
required_pkgs <- c("readxl", "dplyr", "tidyr", "ggplot2", "knitr",
                    "kableExtra", "scales", "forcats", "psych", "janitor")
new_pkgs <- required_pkgs[!(required_pkgs %in% installed.packages()[, "Package"])]
if (length(new_pkgs)) install.packages(new_pkgs)

library(readxl)
library(dplyr)
library(tidyr)
library(ggplot2)
library(knitr)
library(kableExtra)
library(scales)
library(forcats)
library(psych)
library(janitor)

1.2 Load Data

# UPDATE THIS PATH to wherever you saved the file
df <- read_excel("bpsrs_full_dataset_2026.xlsx", sheet = "full dataset (use)")

cat("Rows:", nrow(df), "\n")
## Rows: 1471
cat("Columns:", ncol(df), "\n")
## Columns: 268
cat("Dataset labels:\n")
## Dataset labels:
print(table(df$label))
## 
##   dataset_one dataset_three   dataset_two 
##           213           342           916

1.3 Define Codebook Mappings

All code-to-label mappings below are taken directly from the survey instrument.

# ------------------------------------------------------------------
# Q2: Racial Identity
# ------------------------------------------------------------------
q2_labels <- c(
  "1" = "American Indian or Alaskan Native",
  "2" = "Asian American",
  "3" = "Black/African American",
  "4" = "Native Hawaiian or other Pacific Islander",
  "5" = "White or Caucasian",
  "6" = "Biracial or Multiracial (Black and another race)",
  "7" = "Prefer to self-describe",
  "8" = "African",
  "9" = "Afro-Latina/o/x",
  "10" = "Afro-Caribbean"
)

# ------------------------------------------------------------------
# Q3: Ethnicity
# ------------------------------------------------------------------
q3_labels <- c(
  "1" = "Hispanic or Latina/o/x",
  "2" = "Non-Hispanic or Latina/o/x",
  "3" = "Prefer not to identify"
)

# ------------------------------------------------------------------
# Q4: Gender
# ------------------------------------------------------------------
q4_labels <- c(
  "1" = "Male",
  "2" = "Female",
  "3" = "Non-binary / third gender",
  "4" = "Transgender",
  "5" = "Prefer to self-describe",
  "6" = "Prefer not to respond"
)

# ------------------------------------------------------------------
# Q5: Sexual Orientation
# ------------------------------------------------------------------
q5_labels <- c(
  "1" = "Asexual",
  "2" = "Bisexual",
  "3" = "Pansexual",
  "4" = "Gay",
  "5" = "Lesbian",
  "6" = "Heterosexual or straight",
  "7" = "Queer",
  "8" = "Prefer to self-describe",
  "9" = "Prefer not to respond"
)

# ------------------------------------------------------------------
# Q6: Age Group
# ------------------------------------------------------------------
q6_labels <- c(
  "1" = "18-21 years old",
  "2" = "21-34 years old",
  "3" = "35-44 years old",
  "4" = "45-54 years old",
  "5" = "55-64 years old",
  "6" = "65-74 years old",
  "7" = "75 years or older"
)

# ------------------------------------------------------------------
# Q7: Highest Education
# ------------------------------------------------------------------
q7_labels <- c(
  "1" = "Less than high school diploma",
  "2" = "High school diploma or GED",
  "3" = "Some college, but no degree",
  "4" = "Trade/technical/vocational training",
  "5" = "Associate degree",
  "6" = "Bachelor's degree",
  "7" = "Master's degree",
  "8" = "Professional degree (MD, DDS, JD)",
  "9" = "Doctorate degree (PhD, EdD)"
)

# ------------------------------------------------------------------
# Q8: Marital Status
# ------------------------------------------------------------------
q8_labels <- c(
  "1" = "Single, never married",
  "2" = "Married or domestic partnership",
  "3" = "Widowed",
  "4" = "Divorced",
  "5" = "Separated"
)

# ------------------------------------------------------------------
# Q9: Employment Status
# ------------------------------------------------------------------
q9_labels <- c(
  "1" = "Employed full-time",
  "2" = "Employed part-time",
  "3" = "Self-employed",
  "4" = "Out of work, looking",
  "5" = "Out of work, not looking",
  "6" = "Homemaker",
  "7" = "Student",
  "8" = "Military",
  "9" = "Retired",
  "10" = "Unable to work"
)

# ------------------------------------------------------------------
# Q10: Work Location
# ------------------------------------------------------------------
q10_labels <- c(
  "1" = "100% virtual",
  "2" = "Mostly virtual",
  "3" = "Half virtual / half physical",
  "4" = "Mostly physical location",
  "5" = "100% physical location"
)

# ------------------------------------------------------------------
# Q12: Household Income
# ------------------------------------------------------------------
q12_labels <- c(
  "1" = "Less than $10,000",
  "2" = "$10,000 to $14,999",
  "3" = "$15,000 to $24,999",
  "4" = "$25,000 to $34,999",
  "5" = "$35,000 to $49,999",
  "6" = "$50,000 to $74,999",
  "7" = "$75,000 to $99,999",
  "8" = "$100,000 to $149,999",
  "9" = "$150,000 to $199,999",
  "10" = "$200,000 or more"
)

# ------------------------------------------------------------------
# Q13: Professional Development Rating
# ------------------------------------------------------------------
q13_labels <- c(
  "1" = "Very Poor",
  "2" = "Poor",
  "3" = "Unsure or not applicable",
  "4" = "Good",
  "5" = "Excellent"
)

# ------------------------------------------------------------------
# Q14: Hear About Events (Frequency)
# ------------------------------------------------------------------
q14_labels <- c(
  "1" = "Never",
  "2" = "Rarely",
  "3" = "Occasionally",
  "4" = "Frequently",
  "5" = "Very Frequently"
)

# ------------------------------------------------------------------
# Q16: Event Participation Frequency
# ------------------------------------------------------------------
q16_labels <- c(
  "1" = "Never",
  "2" = "Rarely",
  "3" = "Occasionally",
  "4" = "Frequently",
  "5" = "Very Frequently"
)

# ------------------------------------------------------------------
# Q17: Satisfaction with Events
# ------------------------------------------------------------------
q17_labels <- c(
  "1" = "Highly dissatisfied",
  "2" = "Dissatisfied",
  "3" = "Somewhat dissatisfied",
  "4" = "Neither satisfied nor dissatisfied",
  "5" = "Somewhat satisfied",
  "6" = "Satisfied",
  "7" = "Highly satisfied"
)

# ------------------------------------------------------------------
# Q19: Entrepreneurship Support
# ------------------------------------------------------------------
q19_labels <- c(
  "1" = "Very Poor",
  "2" = "Poor",
  "3" = "Unsure",
  "4" = "Good",
  "5" = "Excellent",
  "6" = "Not Applicable"
)

# ------------------------------------------------------------------
# Q20: Aware of Racial Equity Policies
# ------------------------------------------------------------------
q20_labels <- c(
  "1" = "Yes",
  "2" = "No",
  "3" = "Not Sure"
)

# ------------------------------------------------------------------
# Q22: How Came to Pittsburgh
# ------------------------------------------------------------------
q22_labels <- c(
  "1" = "Born in Pittsburgh region",
  "2" = "Not born but raised in Pittsburgh",
  "3" = "Transplant for professional/employment",
  "4" = "Transplant for educational opportunity",
  "5" = "Transplant for loved one/family/friend",
  "6" = "Prefer to self-describe"
)

# ------------------------------------------------------------------
# Q23: Current Residence Status
# ------------------------------------------------------------------
q23_labels <- c(
  "1" = "Yes, no desire to leave",
  "2" = "No, not currently, no plan to return",
  "3" = "Yes, but strong desire to leave",
  "4" = "Yes, after moving back (returnee)",
  "5" = "No, but moving back in next 6-12 months"
)

# ------------------------------------------------------------------
# Q18: Institution Labels
# ------------------------------------------------------------------
q18_institution_labels <- c(
  "Q18_1"  = "Banks/Credit Unions",
  "Q18_2"  = "Charities/Nonprofits",
  "Q18_3"  = "Community Gardens",
  "Q18_4"  = "Cultural Centers",
  "Q18_5"  = "Faith-based Organizations",
  "Q18_6"  = "Fire Departments",
  "Q18_7"  = "Healthcare/Hospitals",
  "Q18_8"  = "K-12 Private Schools",
  "Q18_9"  = "K-12 Public Schools",
  "Q18_10" = "Law Enforcement/Police",
  "Q18_11" = "Legal Systems/Courts",
  "Q18_12" = "Libraries",
  "Q18_13" = "Local Government",
  "Q18_14" = "Media/News Outlets",
  "Q18_15" = "Parks/Recreation",
  "Q18_16" = "Public Transit",
  "Q18_17" = "Real Estate/Housing",
  "Q18_18" = "Small Businesses",
  "Q18_19" = "Social Services",
  "Q18_20" = "Universities/Colleges",
  "Q18_21" = "Utilities"
)

2 Helper Functions

# ------------------------------------------------------------------
# Frequency table for a single categorical variable
# ------------------------------------------------------------------
freq_table <- function(data, var, labels_vec, var_label = NULL) {
  var_sym <- rlang::sym(var)
  
  tbl <- data %>%
    filter(!is.na(!!var_sym)) %>%
    count(Value = as.character(!!var_sym)) %>%
    mutate(
      Label = labels_vec[Value],
      Label = ifelse(is.na(Label), paste0("Code ", Value), Label),
      Valid_Pct = round(n / sum(n) * 100, 1),
      Cumulative_Pct = round(cumsum(n) / sum(n) * 100, 1)
    ) %>%
    select(Value, Label, Count = n, `Valid %` = Valid_Pct, `Cumulative %` = Cumulative_Pct)
  
  n_valid <- sum(!is.na(data[[var]]))
  n_missing <- sum(is.na(data[[var]]))
  
  if (!is.null(var_label)) {
    cat("\n**", var_label, "** (", var, ")\n", sep = "")
  }
  cat("n =", n_valid, "| Missing =", n_missing,
      "(", round(n_missing / nrow(data) * 100, 1), "%)\n\n")
  
  tbl
}

# ------------------------------------------------------------------
# Bar chart for a categorical variable
# ------------------------------------------------------------------
freq_barplot <- function(data, var, labels_vec, title = "", wrap_width = 25) {
  var_sym <- rlang::sym(var)
  
  plot_df <- data %>%
    filter(!is.na(!!var_sym)) %>%
    mutate(
      val = as.character(!!var_sym),
      lab = labels_vec[val],
      lab = ifelse(is.na(lab), paste0("Code ", val), lab),
      lab = stringr::str_wrap(lab, width = wrap_width)
    ) %>%
    count(lab) %>%
    mutate(pct = n / sum(n) * 100)
  
  ggplot(plot_df, aes(x = reorder(lab, n), y = pct)) +
    geom_col(fill = "#2F5496", width = 0.7) +
    geom_text(aes(label = paste0(round(pct, 1), "%")),
              hjust = -0.1, size = 3) +
    coord_flip() +
    scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
    labs(title = title, x = NULL, y = "Percent of Valid Responses") +
    theme_minimal(base_size = 12) +
    theme(plot.title = element_text(face = "bold"))
}

3 Data Quality Overview

missingness <- data.frame(
  Column = names(df),
  Dtype = sapply(df, class),
  n_Valid = sapply(df, function(x) sum(!is.na(x))),
  n_Missing = sapply(df, function(x) sum(is.na(x))),
  Pct_Missing = round(sapply(df, function(x) mean(is.na(x))) * 100, 1),
  Unique = sapply(df, function(x) length(unique(na.omit(x)))),
  stringsAsFactors = FALSE, row.names = NULL
)

cat("Missingness Tiers:\n")
## Missingness Tiers:
cat("  Low (<10%):       ", sum(missingness$Pct_Missing < 10), "columns\n")
##   Low (<10%):        40 columns
cat("  Moderate (10-50%):", sum(missingness$Pct_Missing >= 10 & missingness$Pct_Missing < 50), "columns\n")
##   Moderate (10-50%): 11 columns
cat("  High (50-90%):    ", sum(missingness$Pct_Missing >= 50 & missingness$Pct_Missing < 90), "columns\n")
##   High (50-90%):     34 columns
cat("  Very High (>90%): ", sum(missingness$Pct_Missing >= 90), "columns\n")
##   Very High (>90%):  183 columns
core_vars <- c("Q2","Q3","Q4","Q5","Q6","Q7","Q8","Q9","Q10","Q12",
               "Q13","Q14","Q16","Q17","Q19","Q20","Q22","Q23",
               paste0("Q18_", 1:21))

missingness %>%
  filter(Column %in% core_vars) %>%
  arrange(Pct_Missing) %>%
  kable(caption = "Missingness for Core Variables") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Missingness for Core Variables
Column Dtype n_Valid n_Missing Pct_Missing Unique
Q2 numeric 1471 0 0.0 6
Q6 numeric 1460 11 0.7 7
Q7 numeric 1461 10 0.7 9
Q4 numeric 1459 12 0.8 6
Q5 numeric 1458 13 0.9 9
Q9 numeric 1458 13 0.9 10
Q8 numeric 1454 17 1.2 5
Q3 numeric 1445 26 1.8 3
Q17 numeric 1435 36 2.4 7
Q20 numeric 1435 36 2.4 3
Q14 numeric 1432 39 2.7 5
Q16 numeric 1431 40 2.7 5
Q18_3 numeric 1431 40 2.7 5
Q12 numeric 1430 41 2.8 10
Q13 numeric 1428 43 2.9 5
Q18_1 numeric 1429 42 2.9 5
Q18_4 numeric 1428 43 2.9 5
Q18_5 numeric 1428 43 2.9 5
Q18_7 numeric 1429 42 2.9 5
Q18_6 numeric 1426 45 3.1 5
Q18_9 numeric 1426 45 3.1 5
Q18_11 numeric 1426 45 3.1 5
Q18_8 numeric 1424 47 3.2 5
Q18_2 numeric 1423 48 3.3 5
Q18_12 numeric 1422 49 3.3 5
Q18_13 numeric 1422 49 3.3 5
Q18_17 numeric 1422 49 3.3 5
Q18_19 numeric 1423 48 3.3 5
Q18_20 numeric 1423 48 3.3 5
Q18_14 numeric 1421 50 3.4 5
Q18_18 numeric 1421 50 3.4 5
Q18_10 numeric 1420 51 3.5 5
Q18_15 numeric 1420 51 3.5 5
Q18_21 numeric 1420 51 3.5 5
Q18_16 numeric 1417 54 3.7 5
Q23 numeric 1372 99 6.7 5
Q22 numeric 1366 105 7.1 6
Q10 numeric 1284 187 12.7 5
Q19 numeric 1255 216 14.7 5

4 Demographic Frequency Tables

4.1 Racial Identity (Q2)

freq_table(df, "Q2", q2_labels, "Racial Identity") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Racial Identity** (Q2)
## n = 1471 | Missing = 0 ( 0 %)
Value Label Count Valid % Cumulative %
10 Afro-Caribbean 17 1.2 1.2
3 Black/African American 1346 91.5 92.7
6 Biracial or Multiracial (Black and another race) 69 4.7 97.3
7 Prefer to self-describe 3 0.2 97.6
8 African 31 2.1 99.7
9 Afro-Latina/o/x 5 0.3 100.0
freq_barplot(df, "Q2", q2_labels, "Racial Identity (Q2)")

4.2 Ethnicity (Q3)

freq_table(df, "Q3", q3_labels, "Ethnicity") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Ethnicity** (Q3)
## n = 1445 | Missing = 26 ( 1.8 %)
Value Label Count Valid % Cumulative %
1 Hispanic or Latina/o/x 235 16.3 16.3
2 Non-Hispanic or Latina/o/x 1167 80.8 97.0
3 Prefer not to identify 43 3.0 100.0
freq_barplot(df, "Q3", q3_labels, "Ethnicity (Q3)")

4.3 Gender (Q4)

freq_table(df, "Q4", q4_labels, "Gender") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Gender** (Q4)
## n = 1459 | Missing = 12 ( 0.8 %)
Value Label Count Valid % Cumulative %
1 Male 870 59.6 59.6
2 Female 575 39.4 99.0
3 Non-binary / third gender 6 0.4 99.5
4 Transgender 4 0.3 99.7
5 Prefer to self-describe 2 0.1 99.9
6 Prefer not to respond 2 0.1 100.0
freq_barplot(df, "Q4", q4_labels, "Gender (Q4)")

4.4 Sexual Orientation (Q5)

freq_table(df, "Q5", q5_labels, "Sexual Orientation") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Sexual Orientation** (Q5)
## n = 1458 | Missing = 13 ( 0.9 %)
Value Label Count Valid % Cumulative %
1 Asexual 84 5.8 5.8
2 Bisexual 101 6.9 12.7
3 Pansexual 20 1.4 14.1
4 Gay 40 2.7 16.8
5 Lesbian 20 1.4 18.2
6 Heterosexual or straight 1163 79.8 97.9
7 Queer 13 0.9 98.8
8 Prefer to self-describe 4 0.3 99.1
9 Prefer not to respond 13 0.9 100.0
freq_barplot(df, "Q5", q5_labels, "Sexual Orientation (Q5)")

4.5 Age Group (Q6)

freq_table(df, "Q6", q6_labels, "Age Group") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Age Group** (Q6)
## n = 1460 | Missing = 11 ( 0.7 %)
Value Label Count Valid % Cumulative %
1 18-21 years old 53 3.6 3.6
2 21-34 years old 647 44.3 47.9
3 35-44 years old 490 33.6 81.5
4 45-54 years old 151 10.3 91.8
5 55-64 years old 72 4.9 96.8
6 65-74 years old 34 2.3 99.1
7 75 years or older 13 0.9 100.0
freq_barplot(df, "Q6", q6_labels, "Age Group (Q6)")

4.6 Highest Education (Q7)

freq_table(df, "Q7", q7_labels, "Highest Education") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Highest Education** (Q7)
## n = 1461 | Missing = 10 ( 0.7 %)
Value Label Count Valid % Cumulative %
1 Less than high school diploma 29 2.0 2.0
2 High school diploma or GED 109 7.5 9.4
3 Some college, but no degree 156 10.7 20.1
4 Trade/technical/vocational training 89 6.1 26.2
5 Associate degree 159 10.9 37.1
6 Bachelor’s degree 516 35.3 72.4
7 Master’s degree 295 20.2 92.6
8 Professional degree (MD, DDS, JD) 50 3.4 96.0
9 Doctorate degree (PhD, EdD) 58 4.0 100.0
freq_barplot(df, "Q7", q7_labels, "Highest Education (Q7)")

4.7 Marital Status (Q8)

freq_table(df, "Q8", q8_labels, "Marital Status") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Marital Status** (Q8)
## n = 1454 | Missing = 17 ( 1.2 %)
Value Label Count Valid % Cumulative %
1 Single, never married 445 30.6 30.6
2 Married or domestic partnership 878 60.4 91.0
3 Widowed 35 2.4 93.4
4 Divorced 71 4.9 98.3
5 Separated 25 1.7 100.0
freq_barplot(df, "Q8", q8_labels, "Marital Status (Q8)")

4.8 Employment Status (Q9)

freq_table(df, "Q9", q9_labels, "Employment Status") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Employment Status** (Q9)
## n = 1458 | Missing = 13 ( 0.9 %)
Value Label Count Valid % Cumulative %
1 Employed full-time 960 65.8 65.8
10 Unable to work 4 0.3 66.1
2 Employed part-time 197 13.5 79.6
3 Self-employed 148 10.2 89.8
4 Out of work, looking 40 2.7 92.5
5 Out of work, not looking 10 0.7 93.2
6 Homemaker 11 0.8 94.0
7 Student 48 3.3 97.3
8 Military 3 0.2 97.5
9 Retired 37 2.5 100.0
freq_barplot(df, "Q9", q9_labels, "Employment Status (Q9)")

4.9 Work Location (Q10)

freq_table(df, "Q10", q10_labels, "Work Location") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Work Location** (Q10)
## n = 1284 | Missing = 187 ( 12.7 %)
Value Label Count Valid % Cumulative %
1 100% virtual 125 9.7 9.7
2 Mostly virtual 182 14.2 23.9
3 Half virtual / half physical 291 22.7 46.6
4 Mostly physical location 338 26.3 72.9
5 100% physical location 348 27.1 100.0
freq_barplot(df, "Q10", q10_labels, "Work Location (Q10)")

4.10 Household Income (Q12)

freq_table(df, "Q12", q12_labels, "Household Income") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Household Income** (Q12)
## n = 1430 | Missing = 41 ( 2.8 %)
Value Label Count Valid % Cumulative %
1 Less than $10,000 69 4.8 4.8
10 $200,000 or more 110 7.7 12.5
2 $10,000 to $14,999 67 4.7 17.2
3 $15,000 to $24,999 88 6.2 23.4
4 $25,000 to $34,999 105 7.3 30.7
5 $35,000 to $49,999 160 11.2 41.9
6 $50,000 to $74,999 260 18.2 60.1
7 $75,000 to $99,999 238 16.6 76.7
8 $100,000 to $149,999 237 16.6 93.3
9 $150,000 to $199,999 96 6.7 100.0
freq_barplot(df, "Q12", q12_labels, "Household Income (Q12)")

4.11 How Came to Pittsburgh (Q22)

freq_table(df, "Q22", q22_labels, "How Came to Pittsburgh") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **How Came to Pittsburgh** (Q22)
## n = 1366 | Missing = 105 ( 7.1 %)
Value Label Count Valid % Cumulative %
1 Born in Pittsburgh region 767 56.1 56.1
2 Not born but raised in Pittsburgh 220 16.1 72.3
3 Transplant for professional/employment 191 14.0 86.2
4 Transplant for educational opportunity 93 6.8 93.0
5 Transplant for loved one/family/friend 72 5.3 98.3
6 Prefer to self-describe 23 1.7 100.0
freq_barplot(df, "Q22", q22_labels, "How Came to Pittsburgh (Q22)")

4.12 Current Residence Status (Q23)

freq_table(df, "Q23", q23_labels, "Current Residence Status") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Current Residence Status** (Q23)
## n = 1372 | Missing = 99 ( 6.7 %)
Value Label Count Valid % Cumulative %
1 Yes, no desire to leave 858 62.5 62.5
2 No, not currently, no plan to return 158 11.5 74.1
3 Yes, but strong desire to leave 210 15.3 89.4
4 Yes, after moving back (returnee) 71 5.2 94.5
5 No, but moving back in next 6-12 months 75 5.5 100.0
freq_barplot(df, "Q23", q23_labels, "Current Residence Status (Q23)")

5 Institution Experience Ratings (Q18)

Q18 asked respondents to rate their overall experience with 21 local institutions. The scale runs from 1 (lowest) to 6 (highest).

5.1 Summary Statistics

q18_cols <- grep("^Q18_\\d+$", names(df), value = TRUE)

q18_summary <- data.frame(
  Variable    = q18_cols,
  Institution = q18_institution_labels[q18_cols],
  stringsAsFactors = FALSE
)

q18_stats <- df %>%
  select(all_of(q18_cols)) %>%
  psych::describe() %>%
  as.data.frame() %>%
  mutate(Variable = q18_cols) %>%
  select(Variable, n, mean, sd, median, min, max, range, skew, kurtosis)

q18_summary <- q18_summary %>%
  left_join(q18_stats, by = "Variable") %>%
  mutate(
    IQR = sapply(q18_cols, function(x) IQR(df[[x]], na.rm = TRUE)),
    Q1  = sapply(q18_cols, function(x) quantile(df[[x]], 0.25, na.rm = TRUE)),
    Q3  = sapply(q18_cols, function(x) quantile(df[[x]], 0.75, na.rm = TRUE)),
    Pct_Low  = sapply(q18_cols, function(x) round(mean(df[[x]] %in% c(1, 2), na.rm = TRUE) * 100, 1)),
    Pct_High = sapply(q18_cols, function(x) round(mean(df[[x]] %in% c(5, 6), na.rm = TRUE) * 100, 1))
  ) %>%
  arrange(mean)

q18_summary %>%
  mutate(across(where(is.numeric) & !c(n), ~ round(.x, 3))) %>%
  kable(caption = "Q18 Institution Ratings – Descriptive Statistics (sorted by mean, ascending)") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE, font_size = 12) %>%
  scroll_box(width = "100%")
Q18 Institution Ratings – Descriptive Statistics (sorted by mean, ascending)
Variable Institution n mean sd median min max range skew kurtosis IQR Q1 Q3 Pct_Low Pct_High
Q18_7 Healthcare/Hospitals 1429 4.430 1.270 5 1 6 5 -0.976 0.650 1.00 4 5.00 5.4 57.9
Q18_20 Universities/Colleges 1423 4.455 1.253 5 1 6 5 -1.009 0.881 1.00 4 5.00 5.3 56.2
Q18_5 Faith-based Organizations 1428 4.509 1.231 5 1 6 5 -1.051 1.001 1.00 4 5.00 4.8 59.2
Q18_10 Law Enforcement/Police 1420 4.529 1.242 5 1 6 5 -0.994 0.850 1.00 4 5.00 4.6 58.2
Q18_16 Public Transit 1417 4.535 1.318 5 1 6 5 -1.098 0.777 1.00 4 5.00 5.8 62.4
Q18_17 Real Estate/Housing 1422 4.563 1.139 5 1 6 5 -0.957 1.197 1.00 4 5.00 3.3 56.7
Q18_19 Social Services 1423 4.591 1.119 5 1 6 5 -1.026 1.413 1.00 4 5.00 3.1 58.9
Q18_12 Libraries 1422 4.605 1.216 5 1 6 5 -1.172 1.281 1.00 4 5.00 4.4 64.2
Q18_6 Fire Departments 1426 4.623 1.115 5 1 6 5 -1.029 1.549 1.00 4 5.00 3.1 58.3
Q18_2 Charities/Nonprofits 1423 4.625 1.153 5 1 6 5 -1.081 1.317 1.00 4 5.00 3.3 62.1
Q18_13 Local Government 1422 4.634 1.156 5 1 6 5 -1.084 1.359 1.00 4 5.00 3.4 61.6
Q18_14 Media/News Outlets 1421 4.657 1.186 5 1 6 5 -1.215 1.489 1.00 4 5.00 3.9 66.0
Q18_18 Small Businesses 1421 4.676 1.102 5 1 6 5 -1.103 1.687 1.00 4 5.00 2.9 62.0
Q18_3 Community Gardens 1431 4.688 1.217 5 1 6 5 -1.265 1.479 1.00 4 5.00 4.2 68.7
Q18_21 Utilities 1420 4.688 1.143 5 1 6 5 -1.197 1.731 1.00 4 5.00 3.4 64.2
Q18_1 Banks/Credit Unions 1429 4.709 1.164 5 1 6 5 -1.413 2.116 1.00 4 5.00 4.0 70.8
Q18_4 Cultural Centers 1428 4.727 1.153 5 1 6 5 -1.335 1.922 1.00 4 5.00 3.5 70.1
Q18_9 K-12 Public Schools 1426 4.774 1.157 5 1 6 5 -1.483 2.400 1.00 4 5.00 3.9 72.6
Q18_8 K-12 Private Schools 1424 4.778 1.092 5 1 6 5 -1.227 1.882 1.25 4 5.25 2.5 68.7
Q18_11 Legal Systems/Courts 1426 4.814 1.144 5 1 6 5 -1.405 2.215 2.00 4 6.00 3.3 72.0
Q18_15 Parks/Recreation 1420 4.850 1.117 5 1 6 5 -1.503 2.726 1.00 5 6.00 3.3 72.9

5.2 Mean Ratings – Horizontal Bar Chart

q18_summary %>%
  ggplot(aes(x = reorder(Institution, mean), y = mean)) +
  geom_col(fill = "#2F5496", width = 0.7) +
  geom_errorbar(aes(ymin = mean - sd, ymax = mean + sd), width = 0.3, color = "gray40") +
  geom_text(aes(label = round(mean, 2)), hjust = -0.3, size = 3) +
  coord_flip() +
  scale_y_continuous(limits = c(0, 7), breaks = 1:6) +
  labs(title = "Q18: Mean Institution Ratings (with ±1 SD)",
       x = NULL, y = "Mean Rating (1-6 scale)") +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"))

5.3 Full Frequency Distributions

q18_freq <- df %>%
  select(all_of(q18_cols)) %>%
  pivot_longer(everything(), names_to = "Variable", values_to = "Rating") %>%
  filter(!is.na(Rating)) %>%
  mutate(Institution = q18_institution_labels[Variable]) %>%
  count(Institution, Rating) %>%
  group_by(Institution) %>%
  mutate(Pct = round(n / sum(n) * 100, 1)) %>%
  ungroup()

q18_freq %>%
  select(Institution, Rating, Count = n, `Valid %` = Pct) %>%
  kable(caption = "Q18 Institution Ratings – Full Frequency Distributions") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE) %>%
  collapse_rows(columns = 1, valign = "top")
Q18 Institution Ratings – Full Frequency Distributions
Institution Rating Count Valid %
Banks/Credit Unions 1 59 4.1
3 159 11.1
4 169 11.8
5 735 51.4
6 307 21.5
Charities/Nonprofits 1 49 3.4
3 181 12.7
4 279 19.6
5 610 42.9
6 304 21.4
Community Gardens 1 62 4.3
3 187 13.1
4 171 11.9
5 665 46.5
6 346 24.2
Cultural Centers 1 52 3.6
3 166 11.6
4 179 12.5
5 702 49.2
6 329 23.0
Faith-based Organizations 1 70 4.9
3 208 14.6
4 279 19.5
5 597 41.8
6 274 19.2
Fire Departments 1 46 3.2
3 137 9.6
4 386 27.1
5 551 38.6
6 306 21.5
Healthcare/Hospitals 1 79 5.5
3 252 17.6
4 246 17.2
5 601 42.1
6 251 17.6
K-12 Private Schools 1 37 2.6
3 148 10.4
4 228 16.0
5 655 46.0
6 356 25.0
K-12 Public Schools 1 57 4.0
3 140 9.8
4 161 11.3
5 721 50.6
6 347 24.3
Law Enforcement/Police 1 67 4.7
3 210 14.8
4 287 20.2
5 550 38.7
6 306 21.5
Legal Systems/Courts 1 49 3.4
3 141 9.9
4 177 12.4
5 669 46.9
6 390 27.3
Libraries 1 64 4.5
3 194 13.6
4 219 15.4
5 643 45.2
6 302 21.2
Local Government 1 50 3.5
3 170 12.0
4 296 20.8
5 591 41.6
6 315 22.2
Media/News Outlets 1 57 4.0
3 182 12.8
4 211 14.8
5 656 46.2
6 315 22.2
Parks/Recreation 1 48 3.4
3 114 8.0
4 186 13.1
5 679 47.8
6 393 27.7
Public Transit 1 86 6.1
3 220 15.5
4 193 13.6
5 600 42.3
6 318 22.4
Real Estate/Housing 1 48 3.4
3 174 12.2
4 366 25.7
5 549 38.6
6 285 20.0
Small Businesses 1 42 3.0
3 140 9.9
4 327 23.0
5 598 42.1
6 314 22.1
Social Services 1 46 3.2
3 165 11.6
4 346 24.3
5 588 41.3
6 278 19.5
Universities/Colleges 1 78 5.5
3 208 14.6
4 311 21.9
5 562 39.5
6 264 18.6
Utilities 1 50 3.5
3 146 10.3
4 280 19.7
5 615 43.3
6 329 23.2

5.4 Heatmap of Rating Distributions

q18_freq %>%
  mutate(Institution = factor(Institution,
    levels = q18_summary$Institution)) %>%
  ggplot(aes(x = factor(Rating), y = Institution, fill = Pct)) +
  geom_tile(color = "white") +
  geom_text(aes(label = paste0(Pct, "%")), size = 3) +
  scale_fill_gradient(low = "#F7FBFF", high = "#2F5496", name = "% of\nResponses") +
  labs(title = "Q18: Distribution of Ratings by Institution",
       x = "Rating", y = NULL) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"))

6 Opinion and Satisfaction Scales

6.1 Professional Development Rating (Q13)

freq_table(df, "Q13", q13_labels, "Professional Development/Career Resources Rating") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Professional Development/Career Resources Rating** (Q13)
## n = 1428 | Missing = 43 ( 2.9 %)
Value Label Count Valid % Cumulative %
1 Very Poor 84 5.9 5.9
2 Poor 245 17.2 23.0
3 Unsure or not applicable 183 12.8 35.9
4 Good 687 48.1 84.0
5 Excellent 229 16.0 100.0
freq_barplot(df, "Q13", q13_labels, "Professional Development Rating (Q13)")

6.2 Hear About Events – Frequency (Q14)

freq_table(df, "Q14", q14_labels, "How Often Hear About Events") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **How Often Hear About Events** (Q14)
## n = 1432 | Missing = 39 ( 2.7 %)
Value Label Count Valid % Cumulative %
1 Never 28 2.0 2.0
2 Rarely 186 13.0 14.9
3 Occasionally 531 37.1 52.0
4 Frequently 518 36.2 88.2
5 Very Frequently 169 11.8 100.0
freq_barplot(df, "Q14", q14_labels, "Hear About Events (Q14)")

6.3 Event Participation Frequency (Q16)

freq_table(df, "Q16", q16_labels, "Event Participation Frequency") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Event Participation Frequency** (Q16)
## n = 1431 | Missing = 40 ( 2.7 %)
Value Label Count Valid % Cumulative %
1 Never 44 3.1 3.1
2 Rarely 242 16.9 20.0
3 Occasionally 557 38.9 58.9
4 Frequently 445 31.1 90.0
5 Very Frequently 143 10.0 100.0
freq_barplot(df, "Q16", q16_labels, "Event Participation Frequency (Q16)")

6.4 Satisfaction with Cultural/Social Events (Q17)

freq_table(df, "Q17", q17_labels, "Satisfaction with Cultural/Social Events") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Satisfaction with Cultural/Social Events** (Q17)
## n = 1435 | Missing = 36 ( 2.4 %)
Value Label Count Valid % Cumulative %
1 Highly dissatisfied 57 4.0 4.0
2 Dissatisfied 108 7.5 11.5
3 Somewhat dissatisfied 189 13.2 24.7
4 Neither satisfied nor dissatisfied 149 10.4 35.1
5 Somewhat satisfied 339 23.6 58.7
6 Satisfied 426 29.7 88.4
7 Highly satisfied 167 11.6 100.0
freq_barplot(df, "Q17", q17_labels, "Satisfaction with Events (Q17)")

6.5 Entrepreneurship Support Ecosystem (Q19)

freq_table(df, "Q19", q19_labels, "Entrepreneurship Support Ecosystem Rating") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Entrepreneurship Support Ecosystem Rating** (Q19)
## n = 1255 | Missing = 216 ( 14.7 %)
Value Label Count Valid % Cumulative %
1 Very Poor 76 6.1 6.1
2 Poor 199 15.9 21.9
3 Unsure 156 12.4 34.3
4 Good 450 35.9 70.2
5 Excellent 374 29.8 100.0
freq_barplot(df, "Q19", q19_labels, "Entrepreneurship Support (Q19)")

6.6 Awareness of Racial Equity Policies (Q20)

freq_table(df, "Q20", q20_labels, "Aware of Racial Equity Policies") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Aware of Racial Equity Policies** (Q20)
## n = 1435 | Missing = 36 ( 2.4 %)
Value Label Count Valid % Cumulative %
1 Yes 654 45.6 45.6
2 No 602 42.0 87.5
3 Not Sure 179 12.5 100.0
freq_barplot(df, "Q20", q20_labels, "Aware of Racial Equity Policies (Q20)")

6.7 Summary Statistics for All Opinion/Satisfaction Scales

opinion_vars <- c("Q13", "Q14", "Q16", "Q17", "Q19")
opinion_labels <- c(
  "Q13" = "Professional Development Rating (1-5)",
  "Q14" = "Hear About Events Frequency (1-5)",
  "Q16" = "Event Participation Frequency (1-5)",
  "Q17" = "Satisfaction with Events (1-7)",
  "Q19" = "Entrepreneurship Support Rating (1-5/6)"
)

opinion_stats <- data.frame(
  Variable = opinion_vars,
  Label = opinion_labels[opinion_vars],
  stringsAsFactors = FALSE
)

opinion_desc <- df %>%
  select(all_of(opinion_vars)) %>%
  psych::describe() %>%
  as.data.frame() %>%
  mutate(Variable = opinion_vars) %>%
  select(Variable, n, mean, sd, median, min, max, skew, kurtosis)

opinion_stats <- opinion_stats %>%
  left_join(opinion_desc, by = "Variable") %>%
  mutate(across(where(is.numeric) & !c(n), ~ round(.x, 3)))

opinion_stats %>%
  kable(caption = "Opinion/Satisfaction Scales – Descriptive Statistics") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
Opinion/Satisfaction Scales – Descriptive Statistics
Variable Label n mean sd median min max skew kurtosis
Q13 Professional Development Rating (1-5) 1428 3.513 1.126 4 1 5 -0.673 -0.471
Q14 Hear About Events Frequency (1-5) 1432 3.429 0.927 3 1 5 -0.205 -0.298
Q16 Event Participation Frequency (1-5) 1431 3.280 0.962 3 1 5 -0.117 -0.375
Q17 Satisfaction with Events (1-7) 1435 4.778 1.630 5 1 7 -0.628 -0.534
Q19 Entrepreneurship Support Rating (1-5/6) 1255 3.675 1.224 4 1 5 -0.675 -0.637

7 Branched Scales (Subgroup-Specific)

These variables are only shown to specific subgroups based on skip logic, so sample sizes are much smaller.

branched_vars <- list(
  Q21 = list(label = "Effectiveness of Racial Equity Policies (1-5)", codes = c("1"="Not effective at all","2"="Somewhat effective","3"="Unsure","4"="Somewhat effective","5"="Extremely effective")),
  Q28 = list(label = "Likelihood of Staying Long-term 7-10yr (1-7)", codes = c("1"="Extremely unlikely","2"="Unlikely","3"="Slightly unlikely","4"="Neither","5"="Slightly likely","6"="Likely","7"="Extremely likely")),
  Q30 = list(label = "Satisfaction with Life in Pittsburgh (1-7)", codes = c("1"="Highly dissatisfied","2"="Dissatisfied","3"="Somewhat dissatisfied","4"="Neither","5"="Somewhat satisfied","6"="Satisfied","7"="Highly satisfied")),
  Q42 = list(label = "Satisfaction with Decision to Return (1-7)", codes = c("1"="Highly dissatisfied","2"="Dissatisfied","3"="Somewhat dissatisfied","4"="Neither","5"="Somewhat satisfied","6"="Satisfied","7"="Highly satisfied")),
  Q67 = list(label = "Satisfaction with Decision to Leave (1-7)", codes = c("1"="Highly dissatisfied","2"="Dissatisfied","3"="Somewhat dissatisfied","4"="Neither","5"="Somewhat satisfied","6"="Satisfied","7"="Highly satisfied"))
)

for (var in names(branched_vars)) {
  info <- branched_vars[[var]]
  s <- df[[var]][!is.na(df[[var]])]
  if (length(s) == 0) next
  
  cat("\n## ", info$label, " (", var, ")\n\n", sep = "")
  cat("n =", length(s), "| Missing =", sum(is.na(df[[var]])), "\n")
  cat("Mean =", round(mean(s), 3), "| Median =", median(s),
      "| SD =", round(sd(s), 3), "| Skew =", round(psych::skew(s), 3), "\n\n")
  
  tbl <- freq_table(df, var, info$codes)
  print(kable(tbl) %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE))
  cat("\n")
}
## 
## ## Effectiveness of Racial Equity Policies (1-5) (Q21)
## 
## n = 787 | Missing = 684 
## Mean = 3.574 | Median = 4 | SD = 1.184 | Skew = -0.547 
## 
## n = 787 | Missing = 684 ( 46.5 %)
## 
## <table class="table table-striped table-hover table-condensed" style="width: auto !important; margin-left: auto; margin-right: auto;">
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Value </th>
##    <th style="text-align:left;"> Label </th>
##    <th style="text-align:right;"> Count </th>
##    <th style="text-align:right;"> Valid % </th>
##    <th style="text-align:right;"> Cumulative % </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> 1 </td>
##    <td style="text-align:left;"> Not effective at all </td>
##    <td style="text-align:right;"> 43 </td>
##    <td style="text-align:right;"> 5.5 </td>
##    <td style="text-align:right;"> 5.5 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 2 </td>
##    <td style="text-align:left;"> Somewhat effective </td>
##    <td style="text-align:right;"> 136 </td>
##    <td style="text-align:right;"> 17.3 </td>
##    <td style="text-align:right;"> 22.7 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 3 </td>
##    <td style="text-align:left;"> Unsure </td>
##    <td style="text-align:right;"> 124 </td>
##    <td style="text-align:right;"> 15.8 </td>
##    <td style="text-align:right;"> 38.5 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 4 </td>
##    <td style="text-align:left;"> Somewhat effective </td>
##    <td style="text-align:right;"> 294 </td>
##    <td style="text-align:right;"> 37.4 </td>
##    <td style="text-align:right;"> 75.9 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 5 </td>
##    <td style="text-align:left;"> Extremely effective </td>
##    <td style="text-align:right;"> 190 </td>
##    <td style="text-align:right;"> 24.1 </td>
##    <td style="text-align:right;"> 100.0 </td>
##   </tr>
## </tbody>
## </table>
## 
## ## Likelihood of Staying Long-term 7-10yr (1-7) (Q28)
## 
## n = 841 | Missing = 630 
## Mean = 5.91 | Median = 6 | SD = 1.262 | Skew = -1.762 
## 
## n = 841 | Missing = 630 ( 42.8 %)
## 
## <table class="table table-striped table-hover table-condensed" style="width: auto !important; margin-left: auto; margin-right: auto;">
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Value </th>
##    <th style="text-align:left;"> Label </th>
##    <th style="text-align:right;"> Count </th>
##    <th style="text-align:right;"> Valid % </th>
##    <th style="text-align:right;"> Cumulative % </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> 1 </td>
##    <td style="text-align:left;"> Extremely unlikely </td>
##    <td style="text-align:right;"> 11 </td>
##    <td style="text-align:right;"> 1.3 </td>
##    <td style="text-align:right;"> 1.3 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 2 </td>
##    <td style="text-align:left;"> Unlikely </td>
##    <td style="text-align:right;"> 19 </td>
##    <td style="text-align:right;"> 2.3 </td>
##    <td style="text-align:right;"> 3.6 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 3 </td>
##    <td style="text-align:left;"> Slightly unlikely </td>
##    <td style="text-align:right;"> 27 </td>
##    <td style="text-align:right;"> 3.2 </td>
##    <td style="text-align:right;"> 6.8 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 4 </td>
##    <td style="text-align:left;"> Neither </td>
##    <td style="text-align:right;"> 26 </td>
##    <td style="text-align:right;"> 3.1 </td>
##    <td style="text-align:right;"> 9.9 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 5 </td>
##    <td style="text-align:left;"> Slightly likely </td>
##    <td style="text-align:right;"> 115 </td>
##    <td style="text-align:right;"> 13.7 </td>
##    <td style="text-align:right;"> 23.5 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 6 </td>
##    <td style="text-align:left;"> Likely </td>
##    <td style="text-align:right;"> 340 </td>
##    <td style="text-align:right;"> 40.4 </td>
##    <td style="text-align:right;"> 64.0 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 7 </td>
##    <td style="text-align:left;"> Extremely likely </td>
##    <td style="text-align:right;"> 303 </td>
##    <td style="text-align:right;"> 36.0 </td>
##    <td style="text-align:right;"> 100.0 </td>
##   </tr>
## </tbody>
## </table>
## 
## ## Satisfaction with Life in Pittsburgh (1-7) (Q30)
## 
## n = 835 | Missing = 636 
## Mean = 5.776 | Median = 6 | SD = 1.112 | Skew = -1.365 
## 
## n = 835 | Missing = 636 ( 43.2 %)
## 
## <table class="table table-striped table-hover table-condensed" style="width: auto !important; margin-left: auto; margin-right: auto;">
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Value </th>
##    <th style="text-align:left;"> Label </th>
##    <th style="text-align:right;"> Count </th>
##    <th style="text-align:right;"> Valid % </th>
##    <th style="text-align:right;"> Cumulative % </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> 1 </td>
##    <td style="text-align:left;"> Highly dissatisfied </td>
##    <td style="text-align:right;"> 5 </td>
##    <td style="text-align:right;"> 0.6 </td>
##    <td style="text-align:right;"> 0.6 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 2 </td>
##    <td style="text-align:left;"> Dissatisfied </td>
##    <td style="text-align:right;"> 7 </td>
##    <td style="text-align:right;"> 0.8 </td>
##    <td style="text-align:right;"> 1.4 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 3 </td>
##    <td style="text-align:left;"> Somewhat dissatisfied </td>
##    <td style="text-align:right;"> 35 </td>
##    <td style="text-align:right;"> 4.2 </td>
##    <td style="text-align:right;"> 5.6 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 4 </td>
##    <td style="text-align:left;"> Neither </td>
##    <td style="text-align:right;"> 37 </td>
##    <td style="text-align:right;"> 4.4 </td>
##    <td style="text-align:right;"> 10.1 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 5 </td>
##    <td style="text-align:left;"> Somewhat satisfied </td>
##    <td style="text-align:right;"> 164 </td>
##    <td style="text-align:right;"> 19.6 </td>
##    <td style="text-align:right;"> 29.7 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 6 </td>
##    <td style="text-align:left;"> Satisfied </td>
##    <td style="text-align:right;"> 378 </td>
##    <td style="text-align:right;"> 45.3 </td>
##    <td style="text-align:right;"> 75.0 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 7 </td>
##    <td style="text-align:left;"> Highly satisfied </td>
##    <td style="text-align:right;"> 209 </td>
##    <td style="text-align:right;"> 25.0 </td>
##    <td style="text-align:right;"> 100.0 </td>
##   </tr>
## </tbody>
## </table>
## 
## ## Satisfaction with Decision to Return (1-7) (Q42)
## 
## n = 69 | Missing = 1402 
## Mean = 4.942 | Median = 5 | SD = 1.765 | Skew = -0.689 
## 
## n = 69 | Missing = 1402 ( 95.3 %)
## 
## <table class="table table-striped table-hover table-condensed" style="width: auto !important; margin-left: auto; margin-right: auto;">
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Value </th>
##    <th style="text-align:left;"> Label </th>
##    <th style="text-align:right;"> Count </th>
##    <th style="text-align:right;"> Valid % </th>
##    <th style="text-align:right;"> Cumulative % </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> 1 </td>
##    <td style="text-align:left;"> Highly dissatisfied </td>
##    <td style="text-align:right;"> 4 </td>
##    <td style="text-align:right;"> 5.8 </td>
##    <td style="text-align:right;"> 5.8 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 2 </td>
##    <td style="text-align:left;"> Dissatisfied </td>
##    <td style="text-align:right;"> 4 </td>
##    <td style="text-align:right;"> 5.8 </td>
##    <td style="text-align:right;"> 11.6 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 3 </td>
##    <td style="text-align:left;"> Somewhat dissatisfied </td>
##    <td style="text-align:right;"> 7 </td>
##    <td style="text-align:right;"> 10.1 </td>
##    <td style="text-align:right;"> 21.7 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 4 </td>
##    <td style="text-align:left;"> Neither </td>
##    <td style="text-align:right;"> 9 </td>
##    <td style="text-align:right;"> 13.0 </td>
##    <td style="text-align:right;"> 34.8 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 5 </td>
##    <td style="text-align:left;"> Somewhat satisfied </td>
##    <td style="text-align:right;"> 12 </td>
##    <td style="text-align:right;"> 17.4 </td>
##    <td style="text-align:right;"> 52.2 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 6 </td>
##    <td style="text-align:left;"> Satisfied </td>
##    <td style="text-align:right;"> 19 </td>
##    <td style="text-align:right;"> 27.5 </td>
##    <td style="text-align:right;"> 79.7 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 7 </td>
##    <td style="text-align:left;"> Highly satisfied </td>
##    <td style="text-align:right;"> 14 </td>
##    <td style="text-align:right;"> 20.3 </td>
##    <td style="text-align:right;"> 100.0 </td>
##   </tr>
## </tbody>
## </table>
## 
## ## Satisfaction with Decision to Leave (1-7) (Q67)
## 
## n = 154 | Missing = 1317 
## Mean = 5.312 | Median = 6 | SD = 1.582 | Skew = -0.919 
## 
## n = 154 | Missing = 1317 ( 89.5 %)
## 
## <table class="table table-striped table-hover table-condensed" style="width: auto !important; margin-left: auto; margin-right: auto;">
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Value </th>
##    <th style="text-align:left;"> Label </th>
##    <th style="text-align:right;"> Count </th>
##    <th style="text-align:right;"> Valid % </th>
##    <th style="text-align:right;"> Cumulative % </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> 1 </td>
##    <td style="text-align:left;"> Highly dissatisfied </td>
##    <td style="text-align:right;"> 5 </td>
##    <td style="text-align:right;"> 3.2 </td>
##    <td style="text-align:right;"> 3.2 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 2 </td>
##    <td style="text-align:left;"> Dissatisfied </td>
##    <td style="text-align:right;"> 6 </td>
##    <td style="text-align:right;"> 3.9 </td>
##    <td style="text-align:right;"> 7.1 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 3 </td>
##    <td style="text-align:left;"> Somewhat dissatisfied </td>
##    <td style="text-align:right;"> 9 </td>
##    <td style="text-align:right;"> 5.8 </td>
##    <td style="text-align:right;"> 13.0 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 4 </td>
##    <td style="text-align:left;"> Neither </td>
##    <td style="text-align:right;"> 20 </td>
##    <td style="text-align:right;"> 13.0 </td>
##    <td style="text-align:right;"> 26.0 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 5 </td>
##    <td style="text-align:left;"> Somewhat satisfied </td>
##    <td style="text-align:right;"> 32 </td>
##    <td style="text-align:right;"> 20.8 </td>
##    <td style="text-align:right;"> 46.8 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 6 </td>
##    <td style="text-align:left;"> Satisfied </td>
##    <td style="text-align:right;"> 40 </td>
##    <td style="text-align:right;"> 26.0 </td>
##    <td style="text-align:right;"> 72.7 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> 7 </td>
##    <td style="text-align:left;"> Highly satisfied </td>
##    <td style="text-align:right;"> 42 </td>
##    <td style="text-align:right;"> 27.3 </td>
##    <td style="text-align:right;"> 100.0 </td>
##   </tr>
## </tbody>
## </table>

8 Multi-Select (Check-All-That-Apply) Items

multiselect_summary <- function(data, item_cols, item_labels, group_label) {
  # Count how many respondents answered at least one item in this block
  group_n <- sum(rowSums(!is.na(data[, item_cols, drop = FALSE])) > 0)
  
  results <- data.frame(
    Variable = item_cols,
    Item = item_labels,
    Times_Selected = sapply(item_cols, function(x) sum(!is.na(data[[x]]))),
    stringsAsFactors = FALSE
  ) %>%
    mutate(
      `% of All (N)` = round(Times_Selected / nrow(data) * 100, 1),
      `% of Group (n)` = round(Times_Selected / group_n * 100, 1)
    ) %>%
    arrange(desc(Times_Selected))
  
  cat("\n**", group_label, "**\n", sep = "")
  cat("Group n (answered at least 1):", group_n, "\n\n")
  
  results
}

8.1 Where Hear About Events (Q15)

q15_items <- paste0("Q15_", 1:9)
q15_labels_ms <- c(
  "Social media", "Word of mouth", "VisitPittsburgh.com",
  "Flyers on the street", "Television", "News",
  "Newspaper", "Radio/podcast", "Other"
)

multiselect_summary(df, q15_items, q15_labels_ms, "Where Hear About Events (Q15)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Where Hear About Events (Q15)**
## Group n (answered at least 1): 1429
Variable Item Times_Selected % of All (N) % of Group (n)
Q15_1 Q15_1 Social media 1049 71.3 73.4
Q15_2 Q15_2 Word of mouth 751 51.1 52.6
Q15_5 Q15_5 Television 444 30.2 31.1
Q15_6 Q15_6 News 393 26.7 27.5
Q15_4 Q15_4 Flyers on the street 390 26.5 27.3
Q15_3 Q15_3 VisitPittsburgh.com 367 24.9 25.7
Q15_7 Q15_7 Newspaper 290 19.7 20.3
Q15_8 Q15_8 Radio/podcast 286 19.4 20.0
Q15_9 Q15_9 Other 55 3.7 3.8

8.2 Why Stayed in Pittsburgh (Q26)

q26_items <- paste0("Q26_", 1:10)
q26_labels_ms <- c(
  "Professional/Job opportunities", "Connection to professional networks",
  "Family/social networks in region", "Educational opportunities",
  "School system", "Cost of living",
  "Satisfied with quality of life", "Regional amenities",
  "Not able to relocate (financial)", "Other"
)

multiselect_summary(df, q26_items, q26_labels_ms, "Why Stayed in Pittsburgh (Q26)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Why Stayed in Pittsburgh (Q26)**
## Group n (answered at least 1): 842
Variable Item Times_Selected % of All (N) % of Group (n)
Q26_1 Q26_1 Professional/Job opportunities 534 36.3 63.4
Q26_3 Q26_3 Family/social networks in region 499 33.9 59.3
Q26_6 Q26_6 Cost of living 467 31.7 55.5
Q26_7 Q26_7 Satisfied with quality of life 376 25.6 44.7
Q26_4 Q26_4 Educational opportunities 342 23.2 40.6
Q26_2 Q26_2 Connection to professional networks 318 21.6 37.8
Q26_5 Q26_5 School system 282 19.2 33.5
Q26_8 Q26_8 Regional amenities 245 16.7 29.1
Q26_9 Q26_9 Not able to relocate (financial) 47 3.2 5.6
Q26_10 Q26_10 Other 23 1.6 2.7

8.3 What Would Make You Leave (Q32)

q32_items <- paste0("Q32_", 1:12)
q32_labels_ms <- c(
  "Employment opportunities", "Chance of advancing career",
  "Better pay elsewhere", "Opportunities for children",
  "Seeking better climate (weather)", "Seeking better climate (socio-political)",
  "Spouse/partner desires a move", "No desire to leave",
  "Experiences of racism/discrimination", "Educational opportunities",
  "Professional opportunities", "Other"
)

multiselect_summary(df, q32_items, q32_labels_ms, "What Would Make You Leave (Q32)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **What Would Make You Leave (Q32)**
## Group n (answered at least 1): 839
Variable Item Times_Selected % of All (N) % of Group (n)
Q32_1 Q32_1 Employment opportunities 316 21.5 37.7
Q32_2 Q32_2 Chance of advancing career 292 19.9 34.8
Q32_3 Q32_3 Better pay elsewhere 287 19.5 34.2
Q32_8 Q32_8 No desire to leave 261 17.7 31.1
Q32_4 Q32_4 Opportunities for children 224 15.2 26.7
Q32_7 Q32_7 Spouse/partner desires a move 219 14.9 26.1
Q32_5 Q32_5 Seeking better climate (weather) 202 13.7 24.1
Q32_9 Q32_9 Experiences of racism/discrimination 186 12.6 22.2
Q32_11 Q32_11 Professional opportunities 184 12.5 21.9
Q32_10 Q32_10 Educational opportunities 145 9.9 17.3
Q32_6 Q32_6 Seeking better climate (socio-political) 134 9.1 16.0
Q32_12 Q32_12 Other 24 1.6 2.9

8.4 Why Left Pittsburgh Originally – Returnees (Q38)

q38_items <- paste0("Q38_", 1:23)
q38_labels_ms <- c(
  "Viable employment opportunities", "Pursuing education",
  "Completed education", "Educational opps for family/social network",
  "Diversity in school system", "Diversity in neighborhoods",
  "Systemic racism in region", "Larger industry footprint elsewhere",
  "Better pay elsewhere", "Region did not feel welcoming",
  "Professional networks", "Personal networks",
  "Regional amenities", "Family networks in another region",
  "Lack of important amenities", "Lack of general amenities (airport, etc.)",
  "Lack of culturally relevant amenities", "Family/partner not embedded",
  "Neighborhood changes (gentrification)", "Health issues",
  "Climate (weather)", "Climate (socio-political)", "Other"
)

multiselect_summary(df, q38_items, q38_labels_ms, "Why Left Pittsburgh Originally – Returnees (Q38)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Why Left Pittsburgh Originally – Returnees (Q38)**
## Group n (answered at least 1): 68
Variable Item Times_Selected % of All (N) % of Group (n)
Q38_1 Q38_1 Viable employment opportunities 26 1.8 38.2
Q38_2 Q38_2 Pursuing education 25 1.7 36.8
Q38_3 Q38_3 Completed education 16 1.1 23.5
Q38_9 Q38_9 Better pay elsewhere 16 1.1 23.5
Q38_7 Q38_7 Systemic racism in region 13 0.9 19.1
Q38_8 Q38_8 Larger industry footprint elsewhere 13 0.9 19.1
Q38_13 Q38_13 Regional amenities 12 0.8 17.6
Q38_15 Q38_15 Lack of important amenities 12 0.8 17.6
Q38_17 Q38_17 Lack of culturally relevant amenities 11 0.7 16.2
Q38_21 Q38_21 Climate (weather) 11 0.7 16.2
Q38_6 Q38_6 Diversity in neighborhoods 9 0.6 13.2
Q38_14 Q38_14 Family networks in another region 9 0.6 13.2
Q38_20 Q38_20 Health issues 9 0.6 13.2
Q38_10 Q38_10 Region did not feel welcoming 8 0.5 11.8
Q38_23 Q38_23 Other 8 0.5 11.8
Q38_5 Q38_5 Diversity in school system 7 0.5 10.3
Q38_12 Q38_12 Personal networks 6 0.4 8.8
Q38_18 Q38_18 Family/partner not embedded 6 0.4 8.8
Q38_22 Q38_22 Climate (socio-political) 6 0.4 8.8
Q38_4 Q38_4 Educational opps for family/social network 5 0.3 7.4
Q38_16 Q38_16 Lack of general amenities (airport, etc.) 5 0.3 7.4
Q38_19 Q38_19 Neighborhood changes (gentrification) 5 0.3 7.4
Q38_11 Q38_11 Professional networks 4 0.3 5.9

8.5 Why Returned to Pittsburgh (Q40)

q40_items <- paste0("Q40_", 1:20)
q40_labels_ms <- c(
  "Viable employment opportunities", "Educational opportunities",
  "Diversity in school system", "Diversity in neighborhoods",
  "Systemic racism in previous region", "Larger industry footprint in Pittsburgh",
  "Better pay in Pittsburgh", "Previous region not welcoming",
  "Professional networks in Pittsburgh", "Personal networks in Pittsburgh",
  "Regional amenities in Pittsburgh", "Family networks in Pittsburgh",
  "Pittsburgh has important amenities", "General amenities (airport, etc.)",
  "Culturally relevant amenities", "Family/partner not embedded in previous region",
  "Neighborhood changes in previous region", "Climate (weather)",
  "Climate (socio-political)", "Other"
)

multiselect_summary(df, q40_items, q40_labels_ms, "Why Returned to Pittsburgh (Q40)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Why Returned to Pittsburgh (Q40)**
## Group n (answered at least 1): 68
Variable Item Times_Selected % of All (N) % of Group (n)
Q40_12 Q40_12 Family networks in Pittsburgh 31 2.1 45.6
Q40_1 Q40_1 Viable employment opportunities 22 1.5 32.4
Q40_2 Q40_2 Educational opportunities 16 1.1 23.5
Q40_16 Q40_16 Family/partner not embedded in previous region 13 0.9 19.1
Q40_10 Q40_10 Personal networks in Pittsburgh 12 0.8 17.6
Q40_9 Q40_9 Professional networks in Pittsburgh 11 0.7 16.2
Q40_13 Q40_13 Pittsburgh has important amenities 11 0.7 16.2
Q40_18 Q40_18 Climate (weather) 11 0.7 16.2
Q40_20 Q40_20 Other 11 0.7 16.2
Q40_6 Q40_6 Larger industry footprint in Pittsburgh 10 0.7 14.7
Q40_7 Q40_7 Better pay in Pittsburgh 10 0.7 14.7
Q40_3 Q40_3 Diversity in school system 9 0.6 13.2
Q40_15 Q40_15 Culturally relevant amenities 9 0.6 13.2
Q40_5 Q40_5 Systemic racism in previous region 8 0.5 11.8
Q40_4 Q40_4 Diversity in neighborhoods 4 0.3 5.9
Q40_11 Q40_11 Regional amenities in Pittsburgh 4 0.3 5.9
Q40_8 Q40_8 Previous region not welcoming 3 0.2 4.4
Q40_14 Q40_14 General amenities (airport, etc.) 3 0.2 4.4
Q40_19 Q40_19 Climate (socio-political) 3 0.2 4.4
Q40_17 Q40_17 Neighborhood changes in previous region 1 0.1 1.5

8.6 What Would Make Returnees Leave Again (Q44)

q44_items <- paste0("Q44_", 1:9)
q44_labels_ms <- c(
  "Employment opportunities", "Chance of advancing career",
  "Better pay elsewhere", "Opportunities for children",
  "Seeking better climate (weather)", "Seeking better climate (socio-political)",
  "Spouse/partner desires a move", "No desire to leave", "Other"
)

multiselect_summary(df, q44_items, q44_labels_ms, "What Would Make Returnees Leave Again (Q44)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **What Would Make Returnees Leave Again (Q44)**
## Group n (answered at least 1): 66
Variable Item Times_Selected % of All (N) % of Group (n)
Q44_1 Q44_1 Employment opportunities 42 2.9 63.6
Q44_3 Q44_3 Better pay elsewhere 41 2.8 62.1
Q44_2 Q44_2 Chance of advancing career 32 2.2 48.5
Q44_7 Q44_7 Spouse/partner desires a move 27 1.8 40.9
Q44_5 Q44_5 Seeking better climate (weather) 25 1.7 37.9
Q44_4 Q44_4 Opportunities for children 23 1.6 34.8
Q44_6 Q44_6 Seeking better climate (socio-political) 19 1.3 28.8
Q44_8 Q44_8 No desire to leave 8 0.5 12.1
Q44_9 Q44_9 Other 3 0.2 4.5

8.7 Why Left Pittsburgh – Former Residents (Q50)

q50_items <- paste0("Q50_", 1:22)
q50_labels_ms <- c(
  "Viable employment opportunities", "Pursuing education",
  "Completed education", "Educational opps for family/social network",
  "Diversity in school system", "Diversity in neighborhoods",
  "Systemic racism in region", "Larger industry footprint elsewhere",
  "Better pay elsewhere", "Region did not feel welcoming",
  "Professional networks", "Personal networks",
  "Regional amenities", "Family networks in another region",
  "Lack of important amenities", "Lack of general amenities (airport, etc.)",
  "Lack of culturally relevant amenities", "Family/partner not embedded",
  "Neighborhood changes (gentrification)", "Health issues",
  "Climate (weather)", "Other"
)

multiselect_summary(df, q50_items, q50_labels_ms, "Why Left Pittsburgh – Former Residents (Q50)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Why Left Pittsburgh – Former Residents (Q50)**
## Group n (answered at least 1): 73
Variable Item Times_Selected % of All (N) % of Group (n)
Q50_1 Q50_1 Viable employment opportunities 28 1.9 38.4
Q50_2 Q50_2 Pursuing education 20 1.4 27.4
Q50_4 Q50_4 Educational opps for family/social network 17 1.2 23.3
Q50_7 Q50_7 Systemic racism in region 17 1.2 23.3
Q50_9 Q50_9 Better pay elsewhere 17 1.2 23.3
Q50_3 Q50_3 Completed education 15 1.0 20.5
Q50_14 Q50_14 Family networks in another region 15 1.0 20.5
Q50_11 Q50_11 Professional networks 12 0.8 16.4
Q50_8 Q50_8 Larger industry footprint elsewhere 11 0.7 15.1
Q50_18 Q50_18 Family/partner not embedded 10 0.7 13.7
Q50_6 Q50_6 Diversity in neighborhoods 9 0.6 12.3
Q50_20 Q50_20 Health issues 9 0.6 12.3
Q50_21 Q50_21 Climate (weather) 8 0.5 11.0
Q50_5 Q50_5 Diversity in school system 7 0.5 9.6
Q50_10 Q50_10 Region did not feel welcoming 6 0.4 8.2
Q50_15 Q50_15 Lack of important amenities 6 0.4 8.2
Q50_12 Q50_12 Personal networks 5 0.3 6.8
Q50_13 Q50_13 Regional amenities 5 0.3 6.8
Q50_16 Q50_16 Lack of general amenities (airport, etc.) 5 0.3 6.8
Q50_17 Q50_17 Lack of culturally relevant amenities 5 0.3 6.8
Q50_19 Q50_19 Neighborhood changes (gentrification) 3 0.2 4.1
Q50_22 Q50_22 Other 2 0.1 2.7

8.8 Why Returning to Pittsburgh – Planned Returnees (Q52)

q52_items <- paste0("Q52_", 1:20)
q52_labels_ms <- c(
  "Viable employment opportunities", "Educational opportunities",
  "Diversity in school system", "Diversity in neighborhoods",
  "Systemic racism in previous region", "Larger industry footprint in Pittsburgh",
  "Better pay in Pittsburgh", "Previous region not welcoming",
  "Professional networks in Pittsburgh", "Personal networks in Pittsburgh",
  "Regional amenities in Pittsburgh", "Family networks in Pittsburgh",
  "Pittsburgh has important amenities", "General amenities (airport, etc.)",
  "Culturally relevant amenities", "Family/partner not embedded in previous region",
  "Neighborhood changes in previous region", "Climate (weather)",
  "Climate (socio-political)", "Other"
)

multiselect_summary(df, q52_items, q52_labels_ms, "Why Returning – Planned Returnees (Q52)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Why Returning – Planned Returnees (Q52)**
## Group n (answered at least 1): 71
Variable Item Times_Selected % of All (N) % of Group (n)
Q52_12 Q52_12 Family networks in Pittsburgh 24 1.6 33.8
Q52_1 Q52_1 Viable employment opportunities 18 1.2 25.4
Q52_2 Q52_2 Educational opportunities 17 1.2 23.9
Q52_10 Q52_10 Personal networks in Pittsburgh 17 1.2 23.9
Q52_8 Q52_8 Previous region not welcoming 16 1.1 22.5
Q52_4 Q52_4 Diversity in neighborhoods 15 1.0 21.1
Q52_5 Q52_5 Systemic racism in previous region 14 1.0 19.7
Q52_7 Q52_7 Better pay in Pittsburgh 13 0.9 18.3
Q52_9 Q52_9 Professional networks in Pittsburgh 13 0.9 18.3
Q52_13 Q52_13 Pittsburgh has important amenities 13 0.9 18.3
Q52_6 Q52_6 Larger industry footprint in Pittsburgh 12 0.8 16.9
Q52_15 Q52_15 Culturally relevant amenities 11 0.7 15.5
Q52_17 Q52_17 Neighborhood changes in previous region 11 0.7 15.5
Q52_3 Q52_3 Diversity in school system 10 0.7 14.1
Q52_11 Q52_11 Regional amenities in Pittsburgh 10 0.7 14.1
Q52_16 Q52_16 Family/partner not embedded in previous region 10 0.7 14.1
Q52_19 Q52_19 Climate (socio-political) 10 0.7 14.1
Q52_14 Q52_14 General amenities (airport, etc.) 9 0.6 12.7
Q52_18 Q52_18 Climate (weather) 6 0.4 8.5
Q52_20 Q52_20 Other 4 0.3 5.6

8.9 Why Want to Leave Pittsburgh (Q56)

q56_items <- paste0("Q56_", 1:23)
q56_labels_ms <- c(
  "Viable employment opportunities", "Pursuing education",
  "Completed education", "Educational opps for family/social network",
  "Diversity in school system", "Diversity in neighborhoods",
  "Systemic racism in region", "Larger industry footprint elsewhere",
  "Better pay elsewhere", "Region did not feel welcoming",
  "Professional networks", "Personal networks",
  "Regional amenities", "Family networks in another region",
  "Lack of important amenities", "Lack of general amenities (airport, etc.)",
  "Lack of culturally relevant amenities", "Family/partner not embedded",
  "Neighborhood changes (gentrification)", "Health issues",
  "Climate (weather)", "Climate (socio-political)", "Other"
)

multiselect_summary(df, q56_items, q56_labels_ms, "Why Want to Leave Pittsburgh (Q56)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Why Want to Leave Pittsburgh (Q56)**
## Group n (answered at least 1): 207
Variable Item Times_Selected % of All (N) % of Group (n)
Q56_7 Q56_7 Systemic racism in region 101 6.9 48.8
Q56_1 Q56_1 Viable employment opportunities 99 6.7 47.8
Q56_9 Q56_9 Better pay elsewhere 97 6.6 46.9
Q56_17 Q56_17 Lack of culturally relevant amenities 65 4.4 31.4
Q56_6 Q56_6 Diversity in neighborhoods 62 4.2 30.0
Q56_21 Q56_21 Climate (weather) 61 4.1 29.5
Q56_5 Q56_5 Diversity in school system 52 3.5 25.1
Q56_15 Q56_15 Lack of important amenities 52 3.5 25.1
Q56_11 Q56_11 Professional networks 46 3.1 22.2
Q56_8 Q56_8 Larger industry footprint elsewhere 45 3.1 21.7
Q56_10 Q56_10 Region did not feel welcoming 42 2.9 20.3
Q56_14 Q56_14 Family networks in another region 41 2.8 19.8
Q56_19 Q56_19 Neighborhood changes (gentrification) 40 2.7 19.3
Q56_2 Q56_2 Pursuing education 37 2.5 17.9
Q56_4 Q56_4 Educational opps for family/social network 36 2.4 17.4
Q56_12 Q56_12 Personal networks 36 2.4 17.4
Q56_13 Q56_13 Regional amenities 35 2.4 16.9
Q56_22 Q56_22 Climate (socio-political) 35 2.4 16.9
Q56_3 Q56_3 Completed education 34 2.3 16.4
Q56_20 Q56_20 Health issues 31 2.1 15.0
Q56_16 Q56_16 Lack of general amenities (airport, etc.) 26 1.8 12.6
Q56_18 Q56_18 Family/partner not embedded 22 1.5 10.6
Q56_23 Q56_23 Other 15 1.0 7.2

8.10 Why Left – Former Residents, No Return Plan (Q65)

q65_items <- paste0("Q65_", 1:20)
q65_labels_ms <- c(
  "Viable employment opportunities", "Education (own)",
  "Diversity in school system", "Diversity in neighborhoods",
  "Systemic racism in region", "Larger industry footprint elsewhere",
  "Better pay elsewhere", "Region did not feel welcoming",
  "Professional networks", "Personal networks",
  "Regional amenities", "Family networks in another region",
  "Lack of important amenities", "Lack of general amenities (airport, etc.)",
  "Lack of culturally relevant amenities", "Family/partner not embedded",
  "Neighborhood changes (gentrification)", "Health issues",
  "Climate", "Other"
)

multiselect_summary(df, q65_items, q65_labels_ms, "Why Left – Former, No Return Plan (Q65)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Why Left – Former, No Return Plan (Q65)**
## Group n (answered at least 1): 156
Variable Item Times_Selected % of All (N) % of Group (n)
Q65_1 Q65_1 Viable employment opportunities 62 4.2 39.7
Q65_7 Q65_7 Better pay elsewhere 59 4.0 37.8
Q65_5 Q65_5 Systemic racism in region 46 3.1 29.5
Q65_6 Q65_6 Larger industry footprint elsewhere 37 2.5 23.7
Q65_2 Q65_2 Education (own) 36 2.4 23.1
Q65_13 Q65_13 Lack of important amenities 32 2.2 20.5
Q65_4 Q65_4 Diversity in neighborhoods 31 2.1 19.9
Q65_15 Q65_15 Lack of culturally relevant amenities 30 2.0 19.2
Q65_3 Q65_3 Diversity in school system 29 2.0 18.6
Q65_8 Q65_8 Region did not feel welcoming 28 1.9 17.9
Q65_11 Q65_11 Regional amenities 27 1.8 17.3
Q65_9 Q65_9 Professional networks 26 1.8 16.7
Q65_19 Q65_19 Climate 24 1.6 15.4
Q65_17 Q65_17 Neighborhood changes (gentrification) 23 1.6 14.7
Q65_10 Q65_10 Personal networks 21 1.4 13.5
Q65_16 Q65_16 Family/partner not embedded 20 1.4 12.8
Q65_14 Q65_14 Lack of general amenities (airport, etc.) 18 1.2 11.5
Q65_12 Q65_12 Family networks in another region 17 1.2 10.9
Q65_18 Q65_18 Health issues 15 1.0 9.6
Q65_20 Q65_20 Other 10 0.7 6.4

9 Industry (Q11)

q11_labels <- c(
  "1" = "Advertising and marketing", "2" = "Agriculture/forestry/fishing/hunting/mining",
  "3" = "Aerospace", "4" = "Construction", "5" = "Computer and technology",
  "6" = "Manufacturing", "7" = "Media and News", "8" = "Retail trade",
  "9" = "Transportation/warehousing/utilities", "10" = "Finance/insurance/real estate",
  "11" = "Waste management", "12" = "Hospitality", "13" = "Education",
  "14" = "Energy", "15" = "Fashion", "16" = "Health care and social assistance",
  "17" = "Entertainment", "18" = "Food services",
  "19" = "Public administration", "20" = "Nonprofit",
  "21" = "Prefer to self-describe"
)

freq_table(df, "Q11", q11_labels, "Industry (Q11)") %>%
  kable() %>% kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
## 
## **Industry (Q11)** (Q11)
## n = 1286 | Missing = 185 ( 12.6 %)
Value Label Count Valid % Cumulative %
1 Advertising and marketing 79 6.1 6.1
10 Finance/insurance/real estate 63 4.9 11.0
11 Waste management 16 1.2 12.3
12 Hospitality 38 3.0 15.2
13 Education 136 10.6 25.8
14 Energy 21 1.6 27.4
15 Fashion 29 2.3 29.7
16 Health care and social assistance 112 8.7 38.4
17 Entertainment 21 1.6 40.0
18 Food services 40 3.1 43.2
19 Public administration 24 1.9 45.0
2 Agriculture/forestry/fishing/hunting/mining 54 4.2 49.2
20 Nonprofit 42 3.3 52.5
21 Prefer to self-describe 44 3.4 55.9
3 Aerospace 17 1.3 57.2
4 Construction 134 10.4 67.7
5 Computer and technology 179 13.9 81.6
6 Manufacturing 90 7.0 88.6
7 Media and News 36 2.8 91.4
8 Retail trade 76 5.9 97.3
9 Transportation/warehousing/utilities 35 2.7 100.0
freq_barplot(df, "Q11", q11_labels, "Industry (Q11)", wrap_width = 30)

10 Dataset Source Comparison

df %>%
  group_by(label) %>%
  summarise(
    n = n(),
    Pct_Female_Q4 = round(mean(Q4 == 2, na.rm = TRUE) * 100, 1),
    Pct_Black_Q2 = round(mean(Q2 == 3, na.rm = TRUE) * 100, 1),
    Mean_Age_Q6 = round(mean(Q6, na.rm = TRUE), 2),
    Mean_Income_Q12 = round(mean(Q12, na.rm = TRUE), 2),
    Pct_Born_Pgh_Q22 = round(mean(Q22 == 1, na.rm = TRUE) * 100, 1),
    Pct_Currently_In_Pgh = round(mean(Q23 %in% c(1, 3, 4), na.rm = TRUE) * 100, 1),
    .groups = "drop"
  ) %>%
  kable(caption = "Key Metrics by Dataset Source",
        col.names = c("Dataset", "n", "% Female", "% Black", "Mean Age Group",
                       "Mean Income Bracket", "% Born in Pittsburgh", "% Currently in Pittsburgh")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
Key Metrics by Dataset Source
Dataset n % Female % Black Mean Age Group Mean Income Bracket % Born in Pittsburgh % Currently in Pittsburgh
dataset_one 213 39.0 91.5 2.99 5.93 49.8 77.5
dataset_three 342 37.4 90.9 2.77 6.35 55.9 76.9
dataset_two 916 40.3 91.7 2.76 6.10 57.8 77.6

11 Session Info

sessionInfo()
## R version 4.5.1 (2025-06-13)
## Platform: aarch64-apple-darwin20
## Running under: macOS Tahoe 26.2
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
## 
## 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/New_York
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] janitor_2.2.1    psych_2.5.6      forcats_1.0.0    scales_1.4.0    
##  [5] kableExtra_1.4.0 knitr_1.50       ggplot2_3.5.2    tidyr_1.3.1     
##  [9] dplyr_1.1.4      readxl_1.4.5    
## 
## loaded via a namespace (and not attached):
##  [1] generics_0.1.4     xml2_1.4.0         stringi_1.8.7      lattice_0.22-7    
##  [5] digest_0.6.37      magrittr_2.0.3     evaluate_1.0.5     grid_4.5.1        
##  [9] timechange_0.3.0   RColorBrewer_1.1-3 fastmap_1.2.0      cellranger_1.1.0  
## [13] purrr_1.1.0        viridisLite_0.4.2  textshaping_1.0.1  jquerylib_0.1.4   
## [17] mnormt_2.1.1       cli_3.6.5          rlang_1.1.6        withr_3.0.2       
## [21] yaml_2.3.10        tools_4.5.1        parallel_4.5.1     vctrs_0.6.5       
## [25] R6_2.6.1           lifecycle_1.0.4    lubridate_1.9.4    snakecase_0.11.1  
## [29] stringr_1.5.2      pkgconfig_2.0.3    pillar_1.11.0      gtable_0.3.6      
## [33] glue_1.8.0         systemfonts_1.2.3  xfun_0.53          tibble_3.3.0      
## [37] tidyselect_1.2.1   rstudioapi_0.17.1  farver_2.1.2       htmltools_0.5.8.1 
## [41] nlme_3.1-168       rmarkdown_2.29     svglite_2.2.1      labeling_0.4.3    
## [45] compiler_4.5.1