---
title: "Measurement Properties of Urban Safety Perception Scales Among University Students in Kyiv"
subtitle: "Analytical Code Report — Working Version"
author:
- name: "Andrii Bova"
date: "March 1, 2026"
format:
html:
theme: [cosmo, custom.scss]
page-layout: article
grid:
sidebar-width: 250px
body-width: 900px
margin-width: 50px
linestretch: 1.8
toc: true
toc-title: "Contents"
toc-depth: 3
toc-location: left
number-sections: true
code-fold: true
code-summary: "▶ Show code"
code-tools: true
df-print: kable
fig-align: center
fig-dpi: 150
self-contained: true
smooth-scroll: true
highlight-style: github
abstract: |
**Background.** Fear of crime and urban safety perception are multidimensional constructs that require psychometrically sound measurement instruments. Despite their widespread use in criminological research, the factor structure and measurement properties of urban safety perception scales have rarely been examined in post-Soviet contexts.
**Objectives.** This study examines the measurement properties of four urban safety perception scales administered to university students in Kyiv: (1) perceived safety across locations, (2) fear and subjective risk of victimisation, (3) neighbourhood disorder, and (4) risk avoidance behaviour.
**Method.** Data were drawn from the survey *Safety Situation in Kyiv — 2016* (*N* = 467 after listwise deletion; 512 students from five Kyiv universities, October–November 2016). The dimensional structure was first explored using Exploratory Graph Analysis (EGA; GLASSO, Walktrap algorithm). Confirmatory factor analysis (CFA) was then conducted for each scale and for an integrated eight-factor model using WLSMV estimation with robust standard errors, appropriate for ordinal indicators. Scale reliability was assessed via ordinal alpha, omega, and categorical composite reliability. Discriminant validity was evaluated using the HTMT₂ criterion.
**Results.** EGA identified four stable dimensions, replicated in 93.4% of 500 bootstrap samples (median = 4, 95% CI [3.51, 4.49]). CFA confirmed the hypothesised factor structures across all four scales. Individual-scale fit was acceptable to excellent: CFI(robust) ranged from .955 to .989, RMSEA(robust) from .067 to .080. The integrated eight-factor model showed adequate fit (CFI = .917, TLI = .903, RMSEA = .064). Composite reliability (ω) ranged from .698 (GRA) to .854 (FCV), and all HTMT₂ values fell below the .85 threshold, supporting discriminant validity across all factor pairs.
**Conclusion.** The four urban safety perception scales demonstrated adequate to good measurement properties in a Kyiv student sample. The findings support the use of these instruments in further research on fear of crime and safety perception in urban Ukrainian contexts.
keywords:
- urban safety
- safety perception
- perceived safety
- fear of crime
- victimisation risk
- neighbourhood disorder
- avoidance behaviour
- factor structure
- scale validation
- measurement properties
- construct validity
- reliability
- confirmatory factor analysis
- WLSMV
- polychoric correlation
- ordinal data
- university students
- Kyiv
- Ukraine
execute:
echo: true
warning: false
message: false
cache: false
knitr:
opts_chunk:
comment: "#>"
fig.width: 9
fig.height: 6
out.width: "100%"
---
```{=html}
<style>
/* ── Typography ─────────────────────────────────────────────────── */
body {
font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
color: #2c3e50;
}
/* Заголовки */
h1 { font-size: 2.1em !important; color: #1a3a5c; font-weight: 700; }
h2 { font-size: 1.65em !important; color: #1a3a5c; font-weight: 700;
border-bottom: 2px solid #3498db; padding-bottom: 6px; margin-top: 2.2em; }
h3 { font-size: 1.4em !important; color: #2471a3; font-weight: 700; }
h4 { font-size: 1.25em !important; color: #1a3a5c; font-weight: 700; }
/* ── Info callout boxes ─────────────────────────────────────────── */
.note-box {
background: #eaf4fb;
border-left: 5px solid #3498db;
padding: 12px 18px;
border-radius: 5px;
margin: 18px 0;
font-size: 1rem;
}
.result-box {
background: #eafaf1;
border-left: 5px solid #27ae60;
padding: 12px 18px;
border-radius: 5px;
margin: 18px 0;
font-size: 1rem;
}
.warn-box {
background: #fef9e7;
border-left: 5px solid #f39c12;
padding: 12px 18px;
border-radius: 5px;
margin: 18px 0;
font-size: 1rem;
}
/* ── Tables ─────────────────────────────────────────────────────── */
table { font-size: 1.05rem !important; }
thead tr { background-color: #1a3a5c !important; color: #ffffff !important; }
tbody tr:nth-child(even) { background-color: #f4f8fc; }
tbody tr:hover { background-color: #dbeafe; transition: background 0.15s; }
/* ── Code blocks ────────────────────────────────────────────────── */
pre, code { font-size: 0.92em; line-height: 1.65; }
/* ── Nav tabs ───────────────────────────────────────────────────── */
.nav-tabs .nav-link.active {
background-color: #1a3a5c;
color: #fff;
border-color: #1a3a5c;
}
/* ── Model section header ───────────────────────────────────────── */
.model-header {
background: #f0f6ff;
border: 1px solid #aed6f1;
border-radius: 6px;
padding: 10px 16px;
margin-bottom: 16px;
font-size: 1.05rem;
}
/* ── Footer badge ───────────────────────────────────────────────── */
.pkg-badge {
display: inline-block;
background: #eaf4fb;
color: #1a3a5c;
font-size: 0.85rem;
font-weight: 600;
padding: 3px 9px;
border-radius: 12px;
border: 1px solid #aed6f1;
margin: 3px;
}
</style>
```
```{r instruments-table}
#| label: tbl-instruments
#| tbl-cap: "Measurement Instruments: Items and Response Alternatives"
#| echo: true
#| code-fold: true
#| warning: false
#| message: false
library(tibble)
library(knitr)
library(kableExtra)
library(dplyr)
# ── Response scales (bilingual) ───────────────────────────────────────
resp_safety <- "1 — Зовсім безпечно / *Completely safe*
2 — Безпечно / *Safe*
3 — Небезпечно / *Unsafe*
4 — Зовсім небезпечно / *Completely unsafe*"
resp_fear <- "1 — Зовсім не боюся / *Not afraid at all*
2 — Скоріше не боюся / *Rather not afraid*
3 — Скоріше боюся / *Rather afraid*
4 — Дуже боюся / *Very afraid*"
resp_risk <- "1 — Зовсім неймовірно / *Very unlikely*
2 — Не дуже ймовірно / *Rather unlikely*
3 — Ймовірно / *Likely*
4 — Дуже ймовірно / *Very likely*"
resp_disorder <- "1 — Не проблема взагалі / *Not a problem at all*
2 — Незначна проблема / *A minor problem*
3 — Велика проблема / *A big problem*
4 — Дуже велика проблема / *A very big problem*"
resp_bhv <- "1 — Майже ніколи / *Almost never*
2 — Іноді / *Sometimes*
3 — Часто / *Often*
4 — Майже завжди / *Almost always*"
# ── Item table ────────────────────────────────────────────────────────
instruments <- tribble(
~Code, ~Scale, ~Ukrainian_Formulation, ~English_Formulation, ~Response_Scale,
# ── Scale 1: Perceived Safety ───────────────────────────────────────
"safd2", "Scale 1: Perceived Safety",
"13. Наскільки безпечно Ви почуваєтесь на самоті у світлий час доби йдучи в районі, де Ви мешкаєте?",
"13. How safe do you feel alone during daytime walking in the neighbourhood where you live?",
resp_safety,
"safd3", "Scale 1: Perceived Safety",
"14. Наскільки безпечно Ви почуваєтесь на самоті у світлий час доби у магазинах, аптеках?",
"14. How safe do you feel alone during daytime in shops and pharmacies?",
resp_safety,
"safd4", "Scale 1: Perceived Safety",
"15. Наскільки безпечно Ви почуваєтесь на самоті у світлий час доби у місцях відпочинку, дозвілля, розваг?",
"15. How safe do you feel alone during daytime in leisure and entertainment venues?",
resp_safety,
"safd5", "Scale 1: Perceived Safety",
"16. Наскільки безпечно Ви почуваєтесь на самоті у світлий час доби користуючись громадським транспортом міста?",
"16. How safe do you feel alone during daytime using public transport?",
resp_safety,
"safn2", "Scale 1: Perceived Safety",
"18. Наскільки безпечно Ви почуваєтесь на самоті у темний час доби після 22 години йдучи в районі, де Ви мешкаєте?",
"18. How safe do you feel alone at night (after 22:00) walking in the neighbourhood where you live?",
resp_safety,
"safn3", "Scale 1: Perceived Safety",
"19. Наскільки безпечно Ви почуваєтесь на самоті у темний час доби після 22 години у магазинах, аптеках?",
"19. How safe do you feel alone at night (after 22:00) in shops and pharmacies?",
resp_safety,
"safn4", "Scale 1: Perceived Safety",
"20. Наскільки безпечно Ви почуваєтесь на самоті у темний час доби після 22 години у місцях відпочинку, дозвілля, розваг?",
"20. How safe do you feel alone at night (after 22:00) in leisure and entertainment venues?",
resp_safety,
"safn5", "Scale 1: Perceived Safety",
"21. Наскільки безпечно Ви почуваєтесь на самоті у темний час доби після 22 години користуючись громадським транспортом міста?",
"21. How safe do you feel alone at night (after 22:00) using public transport?",
resp_safety,
# ── Scale 2: Fear of Victimisation ──────────────────────────────────
"fcv1", "Scale 2: Fear of Victimisation",
"23. Наскільки Ви боїтеся … хтось здійснить крадіжку з Вашого помешкання?",
"23. How afraid are you … that someone will burglarise your home?",
resp_fear,
"fcv3", "Scale 2: Fear of Victimisation",
"25. Наскільки Ви боїтеся … хтось нападе та застосує фізичну силу?",
"25. How afraid are you … that someone will attack and use physical force against you?",
resp_fear,
"fcv4", "Scale 2: Fear of Victimisation",
"26. Наскільки Ви боїтеся … хтось відбере погрозою або силою якусь річ (телефон)?",
"26. How afraid are you … that someone will rob you by threat or force (e.g. phone)?",
resp_fear,
"fcv5", "Scale 2: Fear of Victimisation",
"27. Наскільки Ви боїтеся … хтось поцупить особисті речі (у транспорті, на вулиці)?",
"27. How afraid are you … that someone will steal your personal belongings (in transport or on the street)?",
resp_fear,
"fcv7", "Scale 2: Fear of Victimisation",
"29. Наскільки Ви боїтеся … хтось заволодіє майном чи грошима шляхом обману?",
"29. How afraid are you … that someone will obtain your property or money through fraud?",
resp_fear,
# ── Scale 2 (cont.): Subjective Risk of Victimisation ───────────────
"rcv1", "Scale 2: Subjective Risk of Victimisation",
"32. Як Ви оцінюєте потенційний ризик … хтось здійснить крадіжку з Вашого помешкання?",
"32. How do you assess the potential risk … that someone will burglarise your home?",
resp_risk,
"rcv3", "Scale 2: Subjective Risk of Victimisation",
"34. Як Ви оцінюєте потенційний ризик … хтось нападе та застосує фізичну силу?",
"34. How do you assess the potential risk … that someone will attack and use physical force?",
resp_risk,
"rcv4", "Scale 2: Subjective Risk of Victimisation",
"35. Як Ви оцінюєте потенційний ризик … хтось відбере погрозою або силою якусь річ (телефон)?",
"35. How do you assess the potential risk … that someone will rob you by threat or force?",
resp_risk,
"rcv5", "Scale 2: Subjective Risk of Victimisation",
"36. Як Ви оцінюєте потенційний ризик … хтось поцупить особисті речі (у транспорті, на вулиці)?",
"36. How do you assess the potential risk … that someone will steal your personal belongings?",
resp_risk,
"rcv7", "Scale 2: Subjective Risk of Victimisation",
"38. Як Ви оцінюєте потенційний ризик … хтось заволодіє майном чи грошима шляхом обману?",
"38. How do you assess the potential risk … that someone will obtain your property through fraud?",
resp_risk,
# ── Scale 3: Neighbourhood Disorder ─────────────────────────────────
"sp1", "Scale 3: Neighbourhood Disorder",
"41. Наскільки проблемою у районі є … галасливі сусіди або галасливі вечірки?",
"41. To what extent is the following a problem in your neighbourhood … noisy neighbours or loud parties?",
resp_disorder,
"sp2", "Scale 3: Neighbourhood Disorder",
"42. Наскільки проблемою у районі є … вандалізм, пошкодження майна або автомобілів?",
"42. … vandalism, damage to property or vehicles?",
resp_disorder,
"sp3", "Scale 3: Neighbourhood Disorder",
"43. Наскільки проблемою у районі є … люди, які вживають або поширюють наркотики?",
"43. … people using or dealing drugs?",
resp_disorder,
"sp4", "Scale 3: Neighbourhood Disorder",
"44. Наскільки проблемою у районі є … хулігани та п'яниці у громадських місцях?",
"44. … hooligans and drunk people in public places?",
resp_disorder,
"sp5", "Scale 3: Neighbourhood Disorder",
"45. Наскільки проблемою у районі є … вигул собак без намордників та повідків?",
"45. … dogs walked without muzzles or leads?",
resp_disorder,
"sp6", "Scale 3: Neighbourhood Disorder",
"46. Наскільки проблемою у районі є … крадіжки з помешкань?",
"46. … residential burglaries?",
resp_disorder,
"sp7", "Scale 3: Neighbourhood Disorder",
"47. Наскільки проблемою у районі є … крадіжки особистих речей (у транспорті, на вулиці)?",
"47. … theft of personal belongings (in transport or on the street)?",
resp_disorder,
"sp8", "Scale 3: Neighbourhood Disorder",
"48. Наскільки проблемою у районі є … напади, побиття?",
"48. … physical attacks and assault?",
resp_disorder,
"sp9", "Scale 3: Neighbourhood Disorder",
"49. Наскільки проблемою у районі є … агресивні водії, порушення правил дорожнього руху?",
"49. … aggressive drivers and traffic violations?",
resp_disorder,
# ── Scale 4: Risk Avoidance Behaviour ───────────────────────────────
"bhv1", "Scale 4: Risk Avoidance Behaviour",
"50. Як часто Ви ведете себе обачливо з незнайомими людьми?",
"50. How often do you behave cautiously around strangers?",
resp_bhv,
"bhv2", "Scale 4: Risk Avoidance Behaviour",
"51. Як часто Ви тримаєтесь подалі від певних місць та зустрічей?",
"51. How often do you avoid certain places and gatherings?",
resp_bhv,
"bhv3", "Scale 4: Risk Avoidance Behaviour",
"52. Як часто Ви уникаєте небезпечних побутових ситуацій?",
"52. How often do you avoid dangerous everyday situations?",
resp_bhv,
"bhv4", "Scale 4: Risk Avoidance Behaviour",
"53. Як часто Ви уникаєте виходити з помешкання після настання темряви?",
"53. How often do you avoid going out after dark?",
resp_bhv,
"bhv5", "Scale 4: Risk Avoidance Behaviour",
"54. Як часто Ви уникаєте користуватися транспортом міста після настання темряви?",
"54. How often do you avoid using public transport after dark?",
resp_bhv
)
# ── Render table ──────────────────────────────────────────────────────
instruments |>
select(Code, Scale, Ukrainian_Formulation, English_Formulation, Response_Scale) |>
kable(
col.names = c("Code", "Scale", "Ukrainian formulation", "English formulation", "Response alternatives"),
align = c("l", "l", "l", "l", "l"),
row.names = FALSE
) |>
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "bordered"),
full_width = TRUE,
font_size = 14
) |>
row_spec(0, bold = TRUE, color = "white", background = "#1a3a5c") |>
column_spec(1, bold = TRUE, width = "5em", extra_css = "white-space: nowrap;") |>
column_spec(2, width = "12em", extra_css = "font-style: italic; color: #2471a3;") |>
column_spec(3, width = "22em") |>
column_spec(4, width = "22em") |>
column_spec(5, width = "14em", extra_css = "font-size: 0.8125rem; color: #555;") |>
pack_rows("Scale 1: Perceived Safety Across Locations (safd2–safn5)",
1, 8, bold = TRUE, background = "#dbeafe", color = "#1a3a5c") |>
pack_rows("Scale 2: Fear of Victimisation (fcv1–fcv7)",
9, 13, bold = TRUE, background = "#d5f5e3", color = "#1a3a5c") |>
pack_rows("Scale 2 (cont.): Subjective Risk of Victimisation (rcv1–rcv7)",
14, 18, bold = TRUE, background = "#d5f5e3", color = "#1a3a5c") |>
pack_rows("Scale 3: Neighbourhood Disorder (sp1–sp9)",
19, 27, bold = TRUE, background = "#fef9e7", color = "#1a3a5c") |>
pack_rows("Scale 4: Risk Avoidance Behaviour (bhv1–bhv5)",
28, 32, bold = TRUE, background = "#fdebd0", color = "#1a3a5c")
```
# Setup & Data {#sec-setup}
## Packages {#sec-packages}
```{r setup}
#| label: setup
#| include: true
# ── Clean workspace ───────────────────────────────────────────────────
rm(list = ls())
# ── Packages ──────────────────────────────────────────────────────────
library(haven) # SPSS data import
library(here) # Portable file paths
library(janitor) # clean_names()
library(dplyr) # Data wrangling
library(tidyr) # pivot_longer()
library(lavaan) # CFA / SEM
library(semTools) # reliability(), compRelSEM()
library(knitr) # kable(), include_graphics()
library(kableExtra) # Styled HTML tables
library(ggplot2) # Plots
library(ggcorrplot) # Correlation heatmaps
library(lavaanPlot) # lavaanPlot(), save_png()
library(sjmisc) # frq() frequency tables
library(EGAnet) # Exploratory Graph Analysis
library(writexl) # Excel export
```
## Data Import {#sec-data}
```{r data-import}
#| label: data-import
# ── 1. Load SPSS dataset ──────────────────────────────────────────────
dataset <- read_sav(here("SAFE2016.sav"))
# ── 2. Standardise column names ───────────────────────────────────────
dataset <- dataset |> clean_names()
# ── 3. Define required variables ─────────────────────────────────────
required_vars <- c(
"safd2","safd3","safd4","safd5",
"safn2","safn3","safn4","safn5",
"fcv1","fcv3","fcv4","fcv5","fcv7",
"rcv1","rcv3","rcv4","rcv5","rcv7",
"sp1","sp2","sp3","sp4","sp5","sp6","sp7","sp8","sp9",
"bhv1","bhv2","bhv3","bhv4","bhv5",
"sex"
)
# ── 4. Diagnostic check ───────────────────────────────────────────────
missing_vars <- setdiff(required_vars, names(dataset))
if (length(missing_vars) > 0) {
cat("⚠ Variables NOT found:\n")
cat(paste(" •", missing_vars, collapse = "\n"), "\n\n")
cat("Available names:\n"); print(names(dataset))
stop("Update 'required_vars' to match actual column names.")
} else {
cat("✔ All required variables found.\n")
}
# ── 5. Select & listwise deletion ─────────────────────────────────────
df <- na.omit(dataset[required_vars])
all_vars <- names(df)[!names(df) %in% "sex"] # substantive items only
cat(sprintf("Analytical sample (listwise deletion): N = %d, variables = %d\n",
nrow(df), ncol(df)))
```
## Shared Helper Functions {#sec-helpers}
```{r helpers}
#| label: helpers
# ── 1. SPSS variable labels → styled table ────────────────────────────
print_labels <- function(data, vars) {
tbl <- data.frame(
Variable = vars,
Label = sapply(vars, function(v) {
lbl <- attr(data[[v]], "label")
if (is.null(lbl)) "(no label)" else as.character(lbl)
}),
stringsAsFactors = FALSE,
row.names = NULL
)
kable(tbl,
col.names = c("Variable", "Question wording (SPSS label)"),
align = c("l", "l"),
row.names = FALSE) |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE, font_size = 14, position = "left") |>
column_spec(1, bold = TRUE, width = "8em",
extra_css = "white-space: nowrap;") |>
column_spec(2, width = "auto")
}
# ── 2. Polychoric correlation — lower triangle ────────────────────────
polychoric_lower <- function(data, vars) {
mat <- data |>
select(all_of(vars)) |>
lavCor(ordered = vars) |>
round(2)
mat[upper.tri(mat)] <- ""
mat
}
# ── 3. Styled correlation table ───────────────────────────────────────
render_cor_table <- function(mat, cap) {
kable(mat, caption = cap, align = "c") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed", "bordered"),
full_width = FALSE, font_size = 14, position = "center") |>
row_spec(0, bold = TRUE, color = "white", background = "#1a3a5c")
}
# ── 4. Model fit indices — ROBUST versions ────────────────────────────
# WLSMV + se = "robust" → report robust fit statistics.
# χ²(scaled) is retained as the test statistic (standard for WLSMV).
# RMSEA, CFI, TLI → robust variants.
# WRMR and SRMR have no robust variant.
fit_indices <- c(
"chisq.scaled", "df.scaled", "pvalue.scaled",
"rmsea.robust", "rmsea.ci.lower.robust", "rmsea.ci.upper.robust",
"cfi.robust", "tli.robust",
"wrmr", "srmr"
)
fit_labels <- c(
"χ²(scaled)", "df", "p-value",
"RMSEA(robust)", "RMSEA CI lower", "RMSEA CI upper",
"CFI(robust)", "TLI(robust)",
"WRMR", "SRMR"
)
render_fit_table <- function(fit_obj, model_label) {
fm <- fitMeasures(fit_obj, fit_indices)
vals <- round(as.numeric(fm), 3)
df_fm <- data.frame(Index = fit_labels, Value = vals)
tbl <- kable(df_fm,
caption = paste("Model fit indices —", model_label),
col.names = c("Fit Index", "Value"),
align = c("l", "r")) |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, font_size = 14, position = "left") |>
row_spec(0, bold = TRUE, color = "white", background = "#1a3a5c")
# Highlight rows meeting excellent-fit thresholds
good_cfi <- which(df_fm$Index %in% c("CFI(robust)", "TLI(robust)") & vals >= 0.95)
good_rmsea <- which(df_fm$Index == "RMSEA(robust)" & vals < 0.06)
good_wrmr <- which(df_fm$Index == "WRMR" & vals < 1.0)
for (r in c(good_cfi, good_rmsea, good_wrmr)) {
tbl <- tbl |> row_spec(r, background = "#eafaf1")
}
tbl
}
# ── 5. Reliability table ──────────────────────────────────────────────
render_reliability_table <- function(fit_obj, model_label) {
round(reliability(fit_obj), 3) |>
as.data.frame() |>
kable(caption = paste("Reliability coefficients —", model_label)) |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, font_size = 14) |>
row_spec(0, bold = TRUE, color = "white", background = "#1a3a5c")
}
# ── 6. Bar chart factory ──────────────────────────────────────────────
create_barplot <- function(data, variables, title) {
data |>
select(all_of(variables)) |>
pivot_longer(cols = everything(),
names_to = "variable",
values_to = "value") |>
mutate(value = as.factor(value)) |>
ggplot(aes(x = value)) +
geom_bar(fill = "#3498db", colour = "#1a6fa3", alpha = 0.85) +
facet_wrap(~ variable, scales = "free_y", ncol = 3) +
labs(title = title,
subtitle = paste0("n = ", nrow(data)),
x = "Response value", y = "Frequency") +
theme_minimal(base_size = 12) +
theme(
strip.text = element_text(size = 10, face = "bold", colour = "#1a3a5c"),
strip.background = element_rect(fill = "#eaf4fb", colour = NA),
plot.title = element_text(size = 13, face = "bold", colour = "#1a3a5c"),
plot.subtitle = element_text(size = 10, colour = "#7f8c8d"),
axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
axis.text.y = element_text(size = 9),
panel.grid.minor = element_blank()
)
}
# ── 7. CFA path diagram → PNG → embed in HTML ─────────────────────────
# lavaanPlot produces an htmlwidget (DiagrammeR/Graphviz).
# In self-contained HTML, htmlwidgets may not render reliably.
# Solution: save to PNG with save_png(), then embed via include_graphics().
# This guarantees the diagram appears in all HTML output modes.
render_path_diagram <- function(fit_obj, file_name,
node_fs = 14, edge_fs = 12) {
pl <- lavaanPlot(
model = fit_obj,
coefs = TRUE,
stand = TRUE,
covs = TRUE,
stars = list(regress = TRUE, latent = TRUE, covs = TRUE),
node_options = list(fontsize = node_fs, shape = "box", fontname = "Arial"),
edge_options = list(fontsize = edge_fs, fontname = "Arial")
)
# Save as high-resolution PNG
save_png(pl, file_name, width = 800, height = 600)
# Embed PNG in document (renders in all HTML modes incl. self-contained)
knitr::include_graphics(file_name)
}
```
---
# Descriptive Statistics {#sec-descriptive}
## Frequency Distributions {#sec-freq}
```{r var-groups}
#| label: var-groups
variable_groups <- list(
list(vars = c("safd2","safd3","safd4","safd5","safn2","safn3","safn4","safn5"),
title = "Perceived safety across locations (Відчуття безпеки в локаціях)"),
list(vars = c("fcv1","fcv3","fcv4","fcv5","fcv7"),
title = "Fear of victimisation (Страх віктимізації)"),
list(vars = c("rcv1","rcv3","rcv4","rcv5","rcv7"),
title = "Subjective risk of victimisation (Суб'єктивний ризик віктимізації)"),
list(vars = c("sp1","sp2","sp3","sp4","sp5","sp6","sp7","sp8","sp9"),
title = "Neighbourhood disorder (Проблеми району)"),
list(vars = c("bhv1","bhv2","bhv3","bhv4","bhv5"),
title = "Risk avoidance behaviour (Стратегії уникнення ризику)")
)
```
```{r freq-plots}
#| label: freq-plots
#| fig.height: 7
#| results: asis
for (g in variable_groups) {
cat("\n###", g$title, "\n\n")
print(create_barplot(df, g$vars, g$title))
cat("\n\n")
print(print_labels(df, g$vars))
cat("\n\n---\n\n")
}
```
## Sex Distribution {#sec-sex}
```{r sex-dist}
#| label: sex-dist
# 1 — male (чоловік), 2 — female (жінка)
sjmisc::frq(df$sex)
```
---
# Polychoric Correlation Matrix {#sec-cor-full}
::: {.note-box}
Polychoric correlations for all 32 substantive items computed via `lavaan::lavCor()` with `ordered = TRUE`. Variable `sex` excluded. Matrix exported to Excel.
:::
```{r cor-full-matrix}
#| label: cor-full-matrix
cor_plot_mat <- df |>
select(all_of(all_vars)) |>
lavCor(ordered = all_vars)
cor_export <- cor_plot_mat |> round(2) |> as.data.frame()
cor_export[upper.tri(cor_export)] <- ""
writexl::write_xlsx(
cbind(Variable = rownames(cor_export), cor_export),
"cor_matrix_all_variables.xlsx"
)
kable(cor_export,
caption = "Polychoric correlation matrix — all substantive items (lower triangle)",
align = "c") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","bordered"),
full_width = FALSE, font_size = 13, position = "center") |>
row_spec(0, bold = TRUE, color = "white", background = "#1a3a5c")
```
```{r cor-heatmap}
#| label: cor-heatmap
#| fig.width: 11
#| fig.height: 10
ggcorrplot(cor_plot_mat,
type = "lower",
hc.order = FALSE,
lab = FALSE,
tl.cex = 11,
outline.col = "white",
colors = c("#c0392b", "#ffffff", "#2980b9"),
ggtheme = theme_minimal(base_size = 13)) +
labs(title = "Polychoric Correlation Matrix — All Constructs",
subtitle = "Lower triangle · red = negative · blue = positive",
fill = "r") +
theme(
plot.title = element_text(face = "bold", colour = "#1a3a5c", size = 15),
plot.subtitle = element_text(colour = "#7f8c8d", size = 12),
legend.title = element_text(size = 11),
axis.text = element_text(size = 11)
)
```
---
# Exploratory Graph Analysis {#sec-ega}
## EGA Solution {#sec-ega-main}
::: {.note-box}
**EGA** (Golino & Epskamp, 2017) estimates the number of latent dimensions via regularised partial correlations (GLASSO). Applied to all 32 substantive items; `sex` excluded.
:::
```{r ega-main}
#| label: ega-main
#| fig.width: 10
#| fig.height: 8
ega_result <- EGA(data = df[, seq_along(all_vars)], corr = "auto", plot.EGA = TRUE)
print(ega_result)
png("plot_ega.png", width = 8, height = 6, units = "in", res = 300)
plot(ega_result)
dev.off()
```
## Bootstrap EGA: Dimensional Stability {#sec-ega-boot}
::: {.note-box}
**Bootstrap EGA** (Golino et al., 2020) assesses the stability of the dimensional
structure via resampling. Polychoric correlations are computed internally from raw
data (`corr = "auto"`) — not from a pre-computed matrix. `iter = 500` bootstrap
samples; `seed = 123` for reproducibility.
Stability criterion: item-dimension assignment frequency ≥ 0.70 indicates a stable
solution (Christensen & Golino, 2021).
:::
```{r bootega-m5}
#| label: bootega-m5
#| fig.width: 10
#| fig.height: 8
#| cache: true
# ── Bootstrap EGA ─────────────────────────────────────────────────────
# corr = "auto" → polychoric correlations computed from raw ordinal data
# (NOT from a pre-computed matrix)
bootEGA(
data = df[, seq_along(all_vars)], # 32 substantive items, sex excluded
corr = "auto", # auto-detects ordinal → polychoric
iter = 500,
seed = 123,
type = "resampling", # resampling (not parametric bootstrap)
plot.typicalStructure = TRUE
)
```
---
# Confirmatory Factor Analysis — Individual Scales {#sec-cfa}
::: {.note-box}
**Estimator:** WLSMV with robust standard errors (`se = "robust"`), appropriate for ordinal indicators (Flora & Curran, 2004).
**Fit indices:** χ²(scaled), df, *p*; RMSEA(robust) with 90% CI; CFI(robust); TLI(robust); WRMR; SRMR.
**Benchmarks:** RMSEA < .060 excellent, < .080 acceptable; CFI/TLI > .950 excellent, > .900 acceptable; WRMR < 1.0 (Brown, 2015). Cells meeting the excellent threshold are highlighted in green.
Full parameter estimates (factor loadings, thresholds, R²) are reported in [Appendix A](#sec-appendix).
:::
---
## Model 1: Perceived Safety Across Locations {#sec-m1}
::: {.model-header}
**Constructs:** `Safety_d` — daytime safety · `Safety_n` — night-time safety
**Method effects:** residual correlations between parallel day/night items for the same location.
:::
**Variables**
| Code | Factor | Indicator | Формулювання запитання |
|------|--------|-----------|------------------------|
| safd2 | Safety_d | neighborhood — day | Наскільки безпечно Ви почуваєтесь на самоті у **світлий час** доби йдучи в районі, де Ви мешкаєте? |
| safd3 | Safety_d | shops — day | … у магазинах, аптеках? |
| safd4 | Safety_d | leisure — day | … у місцях відпочинку, дозвілля, розваг? |
| safd5 | Safety_d | transport — day | … користуючись громадським транспортом міста? |
| safn2 | Safety_n | neighborhood — night | Наскільки безпечно Ви почуваєтесь на самоті у **темний час** доби (після 22:00) йдучи в районі, де Ви мешкаєте? |
| safn3 | Safety_n | shops — night | … у магазинах, аптеках? |
| safn4 | Safety_n | leisure — night | … у місцях відпочинку, дозвілля, розваг? |
| safn5 | Safety_n | transport — night | … користуючись громадським транспортом міста? |
```{r fit-m1}
#| label: fit-m1
model.safety <- "
Safety_d =~ safd2 + safd3 + safd4 + safd5
Safety_n =~ safn2 + safn3 + safn4 + safn5
safd2 ~~ safn2
safd3 ~~ safn3
safd4 ~~ safn4
safd5 ~~ safn5
"
fit.safety <- cfa(model.safety,
data = df,
ordered = TRUE,
estimator = "WLSMV",
std.lv = TRUE,
se = "robust")
```
### Polychoric Correlations {#sec-m1-cor}
```{r cor-m1}
#| label: cor-m1
polychoric_lower(df, c("safd2","safd3","safd4","safd5",
"safn2","safn3","safn4","safn5")) |>
render_cor_table("Polychoric correlations — Perceived safety (day / night)")
```
### Model Fit {#sec-m1-fit}
```{r fit-table-m1}
#| label: fit-table-m1
render_fit_table(fit.safety, "Model 1 — Perceived Safety Across Locations")
```
### Path Diagram {#sec-m1-plot}
```{r plot-m1}
#| label: plot-m1
#| fig.width: 9
#| fig.height: 7
#| out.width: "100%"
render_path_diagram(fit.safety, "pl_safety.png")
```
### Reliability & Validity {#sec-m1-rel}
```{r rel-m1}
#| label: rel-m1
render_reliability_table(fit.safety, "Model 1 — Perceived Safety Across Locations")
cat("\nCategorical composite reliability (compRelSEM):\n")
print(round(compRelSEM(fit.safety), 3))
```
---
## Model 2: Fear & Subjective Risk of Victimisation {#sec-m2}
::: {.model-header}
**Constructs:** `FCV` — fear of crime · `RCV` — subjective risk of victimisation
**Method effects:** residual correlations between parallel fear/risk items for the same crime type.
:::
**Variables**
| Code | Factor | Indicator | Формулювання запитання |
|------|--------|-----------|------------------------|
| fcv1 | FCV | burglary | Наскільки часто Ви **боїтесь** стати жертвою крадіжки з помешкання? |
| fcv3 | FCV | assault | … нападу? |
| fcv4 | FCV | robbery | … пограбування? |
| fcv5 | FCV | theft | … крадіжки? |
| fcv7 | FCV | fraud | … шахрайства? |
| rcv1 | RCV | burglary — risk | Яка **ймовірність**, що Ви станете жертвою крадіжки з помешкання? |
| rcv3 | RCV | assault — risk | … нападу? |
| rcv4 | RCV | robbery — risk | … пограбування? |
| rcv5 | RCV | theft — risk | … крадіжки? |
| rcv7 | RCV | fraud — risk | … шахрайства? |
```{r fit-m2}
#| label: fit-m2
model.fear <- "
FCV =~ fcv1 + fcv5 + fcv7 + fcv3 + fcv4
RCV =~ rcv1 + rcv5 + rcv7 + rcv3 + rcv4
fcv1 ~~ rcv1
fcv3 ~~ rcv3
fcv4 ~~ rcv4
fcv5 ~~ rcv5
fcv7 ~~ rcv7
"
fit.fear <- cfa(model.fear,
data = df,
ordered = TRUE,
estimator = "WLSMV",
std.lv = TRUE,
se = "robust")
```
### Polychoric Correlations {#sec-m2-cor}
```{r cor-m2}
#| label: cor-m2
polychoric_lower(df, c("fcv1","fcv3","fcv4","fcv5","fcv7",
"rcv1","rcv3","rcv4","rcv5","rcv7")) |>
render_cor_table("Polychoric correlations — Fear & subjective risk of victimisation")
```
### Model Fit {#sec-m2-fit}
```{r fit-table-m2}
#| label: fit-table-m2
render_fit_table(fit.fear, "Model 2 — Fear & Subjective Risk of Victimisation")
```
### Path Diagram {#sec-m2-plot}
```{r plot-m2}
#| label: plot-m2
#| fig.width: 9
#| fig.height: 7
#| out.width: "100%"
render_path_diagram(fit.fear, "pl_fear.png")
```
### Reliability & Validity {#sec-m2-rel}
```{r rel-m2}
#| label: rel-m2
render_reliability_table(fit.fear, "Model 2 — Fear & Subjective Risk of Victimisation")
cat("\nCategorical composite reliability (compRelSEM):\n")
print(round(compRelSEM(fit.fear), 3))
```
---
## Model 3: Neighbourhood Disorder {#sec-m3}
::: {.model-header}
**Constructs:** `SCP` — serious crime problems · `SAP` — social annoyance problems
:::
**Variables**
| Code | Factor | Indicator | Формулювання запитання |
|------|--------|-----------|------------------------|
| sp7 | SCP | pickpocketing | Чи є у Вашому районі проблема з кишеньковими крадіжками? |
| sp8 | SCP | physical attacks | … нападами? |
| sp6 | SCP | burglaries | … крадіжками з помешкань? |
| sp9 | SCP | aggressive drivers | … агресивними водіями? |
| sp2 | SAP | vandalism | … вандалізмом? |
| sp1 | SAP | noise | … галасливими сусідами? |
| sp4 | SAP | hooligans | … хуліганами? |
| sp3 | SAP | drugs | … наркотиками? |
| sp5 | SAP | stray dogs | … безпритульними собаками? |
```{r fit-m3}
#| label: fit-m3
model.problems <- "
SCP =~ sp7 + sp8 + sp6 + sp9
SAP =~ sp2 + sp1 + sp4 + sp3 + sp5
"
fit.problems <- cfa(model.problems,
data = df,
ordered = TRUE,
estimator = "WLSMV",
std.lv = TRUE,
se = "robust")
```
### Polychoric Correlations {#sec-m3-cor}
```{r cor-m3}
#| label: cor-m3
polychoric_lower(df, c("sp1","sp2","sp3","sp4","sp5",
"sp6","sp7","sp8","sp9")) |>
render_cor_table("Polychoric correlations — Neighbourhood disorder")
```
### Model Fit {#sec-m3-fit}
```{r fit-table-m3}
#| label: fit-table-m3
render_fit_table(fit.problems, "Model 3 — Neighbourhood Disorder")
```
### Path Diagram {#sec-m3-plot}
```{r plot-m3}
#| label: plot-m3
#| fig.width: 9
#| fig.height: 7
#| out.width: "100%"
render_path_diagram(fit.problems, "pl_problems.png")
```
### Reliability & Validity {#sec-m3-rel}
```{r rel-m3}
#| label: rel-m3
render_reliability_table(fit.problems, "Model 3 — Neighbourhood Disorder")
cat("\nCategorical composite reliability (compRelSEM):\n")
print(round(compRelSEM(fit.problems), 3))
```
---
## Model 4: Risk Avoidance Behaviour {#sec-m4}
::: {.model-header}
**Constructs:** `GRA` — general risk avoidance · `NRA` — nocturnal risk avoidance
:::
**Variables**
| Code | Factor | Indicator | Формулювання запитання |
|------|--------|-----------|------------------------|
| bhv1 | GRA | caution — strangers | Чи намагаєтесь Ви бути **обачливим** з незнайомцями? |
| bhv2 | GRA | avoid places | Чи **уникаєте** Ви певних місць через страх стати жертвою злочину? |
| bhv3 | GRA | avoid risky situations | Чи уникаєте Ви **ризикованих** ситуацій? |
| bhv4 | NRA | avoid going out at night | Чи уникаєте Ви **виходити на вулицю вночі**? |
| bhv5 | NRA | avoid transport at night | Чи уникаєте Ви **транспорту вночі**? |
```{r fit-m4}
#| label: fit-m4
model.behavior <- "
GRA =~ bhv1 + bhv2 + bhv3
NRA =~ bhv4 + bhv5
"
fit.behavior <- cfa(model.behavior,
data = df,
ordered = TRUE,
estimator = "WLSMV",
std.lv = TRUE,
se = "robust")
```
### Polychoric Correlations {#sec-m4-cor}
```{r cor-m4}
#| label: cor-m4
polychoric_lower(df, c("bhv1","bhv2","bhv3","bhv4","bhv5")) |>
render_cor_table("Polychoric correlations — Risk avoidance behaviour")
```
### Model Fit {#sec-m4-fit}
```{r fit-table-m4}
#| label: fit-table-m4
render_fit_table(fit.behavior, "Model 4 — Risk Avoidance Behaviour")
```
### Path Diagram {#sec-m4-plot}
```{r plot-m4}
#| label: plot-m4
#| fig.width: 9
#| fig.height: 7
#| out.width: "100%"
render_path_diagram(fit.behavior, "pl_behavior.png")
```
### Reliability & Validity {#sec-m4-rel}
```{r rel-m4}
#| label: rel-m4
render_reliability_table(fit.behavior, "Model 4 — Risk Avoidance Behaviour")
cat("\nCategorical composite reliability (compRelSEM):\n")
print(round(compRelSEM(fit.behavior), 3))
```
---
# Integrated Measurement Model {#sec-m5}
::: {.note-box}
All eight factors estimated simultaneously to assess inter-factor correlations and overall model fit. Method effects from individual models are retained.
:::
```{r cfa-m5}
#| label: cfa-m5
model.all <- "
Safety_d =~ safd2 + safd3 + safd4 + safd5
Safety_n =~ safn2 + safn3 + safn4 + safn5
safd2 ~~ safn2
safd3 ~~ safn3
safd4 ~~ safn4
safd5 ~~ safn5
FCV =~ fcv1 + fcv5 + fcv7 + fcv3 + fcv4
RCV =~ rcv1 + rcv5 + rcv7 + rcv3 + rcv4
fcv1 ~~ rcv1
fcv3 ~~ rcv3
fcv4 ~~ rcv4
fcv5 ~~ rcv5
fcv7 ~~ rcv7
SCP =~ sp7 + sp8 + sp6 + sp9
SAP =~ sp2 + sp1 + sp4 + sp3 + sp5
GRA =~ bhv1 + bhv2 + bhv3
NRA =~ bhv4 + bhv5
"
fit.all <- cfa(model.all,
data = df,
ordered = TRUE,
estimator = "WLSMV",
std.lv = TRUE,
se = "robust")
```
## Model Fit {#sec-m5-fit}
```{r fit-table-m5}
#| label: fit-table-m5
render_fit_table(fit.all, "Model 5 — Integrated Measurement Model")
```
## Path Diagram {#sec-m5-plot}
```{r plot-m5}
#| label: plot-m5
#| fig.width: 14
#| fig.height: 14
#| out.width: "100%"
render_path_diagram(fit.all, "pl_all.png", node_fs = 16, edge_fs = 13)
```
## Discriminant Validity {#sec-m5-htmt}
::: {.note-box}
**HTMT₂ criterion** (Henseler et al., 2015): values below **0.85** indicate adequate
discriminant validity between factor pairs. Computed via `semTools::htmt()` with
`ordered = TRUE` and `htmt2 = TRUE`.
:::
```{r htmt-m5}
#| label: htmt-m5
# ── HTMT₂ discriminant validity ───────────────────────────────────────
htmt_results <- htmt(
model = model.all,
data = df[, all_vars],
ordered = TRUE,
absolute = TRUE,
htmt2 = TRUE
)
# ── Lower-triangle display ────────────────────────────────────────────
htmt_mat <- as.data.frame(round(htmt_results, 3))
htmt_mat[upper.tri(htmt_mat)] <- ""
# ── Export ────────────────────────────────────────────────────────────
writexl::write_xlsx(
cbind(Factor = rownames(htmt_mat), htmt_mat),
"htmt_results.xlsx"
)
# ── Table ─────────────────────────────────────────────────────────────
kable(htmt_mat,
caption = "HTMT₂ discriminant validity matrix — Integrated Model
(values < 0.85 indicate adequate discriminant validity)",
align = "c") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed", "bordered"),
full_width = FALSE, font_size = 14, position = "center") |>
row_spec(0, bold = TRUE, color = "white", background = "#1a3a5c")
```
---
# Data Export {#sec-export}
```{r export}
#| label: export
write_sav(df, "Urban_Safety_Perception_v1.sav")
cat(sprintf("Saved: Urban_Safety_Perception_v1.sav [%d rows × %d cols]\n",
nrow(df), ncol(df)))
```
---
# Conclusion {#sec-conclusion}
This study examined the factor structure and measurement properties of four
urban safety perception scales using data from a survey of university students
in Kyiv (*N* = 467). The analysis proceeded in two stages: an exploratory
stage using EGA to identify the latent dimensional structure, followed by
confirmatory CFA to evaluate model fit, reliability, and validity for each
scale and for an integrated eight-factor model.
**Dimensional structure.** EGA identified four distinct dimensions corresponding
to the theoretically hypothesised constructs: perceived safety across locations,
fear and subjective risk of victimisation, neighbourhood disorder, and risk
avoidance behaviour. Bootstrap EGA confirmed the stability of this solution
across 500 resampling iterations (replication rate = 93.4%; median dimensions
= 4, 95% CI [3.51, 4.49]). Item-level stability coefficients ranged from 0.934
to 1.000, all exceeding the recommended threshold of 0.70, with minor
instability observed only for the subjective risk items (rcv1–rcv7 = 0.934),
reflecting the theoretical proximity of fear and perceived risk constructs.
**Model fit.** CFA confirmed the two-factor structures within each scale.
Individual-scale models demonstrated acceptable to excellent fit: CFI(robust)
ranged from .955 (Model 3) to .989 (Models 1 and 4), and RMSEA(robust) from
.067 (Model 1) to .080 (Models 3 and 4). The integrated eight-factor model
showed adequate overall fit (χ²(scaled) = 829.85, df = 427; CFI = .917,
TLI = .903, RMSEA = .064, SRMR = .053), though WRMR slightly exceeded the
recommended threshold of 1.0 (WRMR = 1.083), suggesting some residual
misfit in the joint model.
**Reliability.** Composite reliability (ω) was good to excellent for most
subscales: FCV (ω = .854), NRA (ω = .839), Safety_d (ω = .823), Safety_n
(ω = .814), RCV (ω = .820), SCP (ω = .821), and SAP (ω = .737). The lowest
reliability was observed for GRA — General Risk Avoidance (ω = .698), which
is attributable to the small number of indicators (three items) and the
broader behavioural content of this subscale.
Average variance extracted (AVE) exceeded .50 for seven of eight subscales, supporting convergent validity; the only exception was SAP (AVE = .421).
**Discriminant validity.** All HTMT₂ values fell below the recommended
threshold of .85, supporting discriminant validity across all eight factor
pairs. The highest HTMT₂ value was observed between Safety_d and Safety_n
(.830), consistent with the shared method variance introduced by the parallel
day/night item format — a finding addressed in the model through residual
correlations between paired items.
**Limitations.** Several limitations should be noted. First, the sample is
restricted to university students from five Kyiv institutions, limiting
generalisability to broader urban populations. Second, the data were collected
in 2016, prior to significant societal changes in Ukraine, which may affect
the contemporary relevance of the findings. Third, measurement invariance
across demographic subgroups (e.g., sex, year of study) was not examined and
should be addressed in future research.
**Implications.** Overall, the four urban safety perception scales demonstrated
adequate to good measurement properties in the present sample. The instruments
may be used with confidence in further research on fear of crime and safety
perception among Ukrainian university students. Future studies should examine
the replicability of the factor structure in general population samples and
test for measurement invariance across groups and time points.
# References {.unnumbered}
Bova, A. (2020). Fear of crime of Kyiv students: Experience of construction
and use of sociological indices. *Science and Law Enforcement, 4*(50), 171–183.
[In Ukrainian]. <http://naukaipravoohorona.com/journal/ukr/2020_4.pdf>
---
# Supplementary Materials {.unnumbered}
Survey questionnaire & data: [osf.io/4mxje](https://osf.io/4mxje){target="_blank"}
# Appendix A: Full CFA Parameter Estimates {#sec-appendix .unnumbered}
::: {.note-box}
This appendix contains the full `lavaan` output for each model: standardised factor loadings, standard errors, z-values, p-values, thresholds, residual variances, factor correlations, and R² values.
:::
## A.1 Model 1 — Perceived Safety Across Locations {.unnumbered}
```{r app-m1}
#| label: app-m1
summary(fit.safety, fit.measures = TRUE, standardized = TRUE, rsquare = TRUE)
```
## A.2 Model 2 — Fear & Subjective Risk of Victimisation {.unnumbered}
```{r app-m2}
#| label: app-m2
summary(fit.fear, fit.measures = TRUE, standardized = TRUE, rsquare = TRUE)
```
## A.3 Model 3 — Neighbourhood Disorder {.unnumbered}
```{r app-m3}
#| label: app-m3
summary(fit.problems, fit.measures = TRUE, standardized = TRUE, rsquare = TRUE)
```
## A.4 Model 4 — Risk Avoidance Behaviour {.unnumbered}
```{r app-m4}
#| label: app-m4
summary(fit.behavior, fit.measures = TRUE, standardized = TRUE, rsquare = TRUE)
```
## A.5 Model 5 — Integrated Measurement Model {.unnumbered}
```{r app-m5}
#| label: app-m5
summary(fit.all, fit.measures = TRUE, standardized = TRUE, rsquare = TRUE)
```
# Citation {.unnumbered}
::: {.note-box}
**APA Style:**
Bova, A. (2026). *Measurement Properties of Urban Safety Perception Scales Among University Students in Kyiv* [Analytical code report]. RPubs. <https://rpubs.com/bova/safety-perception-scales>
**BibTeX entry:**
```bibtex
@misc{bova2026,
author = {Bova, Andrii},
title = {{Measurement} {Properties} of {Urban} {Safety} {Perception} {Scales} {Among} {University} {Students} in {Kyiv}},
year = {2026},
note = {Analytical code report. RPubs},
url = {https://rpubs.com/bova/safety-perception-scales}
}
```
:::
```{r date-footer}
#| echo: false
date_label <- withr::with_locale(
c(LC_TIME = "English_United States.utf8"),
format(Sys.Date(), "%d %B %Y")
)
```
<div style="text-align:center; margin-top:40px; padding:16px;
background:#f8f9fa; border-radius:6px; font-size:0.9em; color:#555;">
Compiled with R `r getRversion()` · lavaan `r packageVersion("lavaan")` · Quarto ·
Document last updated: `r date_label`
</div>