#install.packages(c("psych", "irr","pwr"))
data_dir <- "/Users/haisaosmanli/Desktop/CORE/SDA/DATA"
excel_files <- list.files(path = data_dir, pattern = "\\.xlsx$", full.names = TRUE)
excel_files
## [1] "/Users/haisaosmanli/Desktop/CORE/SDA/DATA/AD.xlsx"
## [2] "/Users/haisaosmanli/Desktop/CORE/SDA/DATA/AK.xlsx"
## [3] "/Users/haisaosmanli/Desktop/CORE/SDA/DATA/CD.xlsx"
## [4] "/Users/haisaosmanli/Desktop/CORE/SDA/DATA/EM.xlsx"
## [5] "/Users/haisaosmanli/Desktop/CORE/SDA/DATA/RL.xlsx"
## [6] "/Users/haisaosmanli/Desktop/CORE/SDA/DATA/VG.xlsx"
definitions <- read_excel(excel_files[1], sheet = 1)
## New names:
## • `` -> `...1`
## • `` -> `...3`
glimpse(definitions)
## Rows: 81
## Columns: 3
## $ ...1 <chr> …
## $ `Present = 1 point; absent=0 points: Place X in each box to indicate the presence of each indicator (total “X”s at the end in yellow box).` <chr> …
## $ ...3 <chr> …
read_researcher_data <- function(file_path) {
# extract researcher initials (eg. "AD.xlsx" → "AD")
researcher_id <- toupper(str_extract(basename(file_path), "^[A-Z]+"))
# get all sheet names
sheets <- excel_sheets(file_path)
# decide which sheets to read
if (researcher_id == "EM") {
target_sheets <- sheets[2:4] # EM → sheets 2,3 and 4
} else if (researcher_id == "VG") {
target_sheets <- sheets[2:4] # VG → sheets 2, 3, and 4
} else {
target_sheets <- sheets[2] # others → only sheet 2
}
# read and combine data from selected sheets
df_list <- lapply(target_sheets, function(sheet_name) {
df <- read_excel(file_path, sheet = sheet_name) %>%
clean_names() %>%
mutate(
researcher = researcher_id,
source_file = basename(file_path),
# clean sheet name (remove "(Pilot)" and convert to date)
date_collected = suppressWarnings(
ymd(str_trim(str_remove(sheet_name, "\\(.*\\)")))
)
)
return(df)
})
# combine all sheets vertically
bind_rows(df_list)
}
# read all files and merge vertically
metadata_all <- map_dfr(excel_files, read_researcher_data)
# preview the merged data
glimpse(metadata_all)
## Rows: 1,650
## Columns: 16
## $ researcher_initials <chr> "AD", "N/A", "N/A", "N/A", "N/A", "N/A", …
## $ weather_brief_description <chr> "Slightly Rainy, Cloudy", "N/A", "N/A", "…
## $ temperature_celsius <chr> "7", "N/A", "N/A", "N/A", "N/A", "N/A", "…
## $ number_of_photos_taken <chr> "4", "N/A", "N/A", "N/A", "N/A", "N/A", "…
## $ time_start_00_00am_pm <chr> "0.375", "N/A", "N/A", "N/A", "N/A", "N/A…
## $ time_end_00_00am_pm <chr> "0.41666666666666669", "N/A", "N/A", "N/A…
## $ location <chr> "Sunalta", "Sunalta", "Sunalta", "Sunalta…
## $ indicator_type <chr> "N/A", "Physical disorder", "Physical dis…
## $ code <chr> "N/A", "001", "002", "003a", "003b", "004…
## $ indicator_description <chr> "N/A", "Pile of discarded cigarette butts…
## $ present_yes_1 <chr> "N/A", "1", "1", "1", NA, "1", "1", NA, N…
## $ frequency_tally_total <chr> "N/A", "7", "2", "14", NA, "4", "1", NA, …
## $ field_notes_where_applicable <chr> "N/A", NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ researcher <chr> "AD", "AD", "AD", "AD", "AD", "AD", "AD",…
## $ source_file <chr> "AD.xlsx", "AD.xlsx", "AD.xlsx", "AD.xlsx…
## $ date_collected <date> 2025-10-19, 2025-10-19, 2025-10-19, 2025…
metadata_all <- metadata_all %>%
mutate(present_yes_1 = suppressWarnings(as.numeric(present_yes_1))) %>% # convert to numeric if possible
filter(!is.na(present_yes_1)) # keep only numeric (non-NA) values
# drop unwanted columns
metadata_all <- metadata_all %>%
select(
-matches("researcher_initials|weather|temperature|number_of_photos|time_start|time_end|field_notes|source_file")
)
# get rid of double sum
metadata_all <- metadata_all %>%
filter(!is.na(code) & code != "N/A")
metadata_all <- metadata_all %>%
#ensure frequency_tally_total is numeric
mutate(frequency_tally_total = suppressWarnings(as.numeric(frequency_tally_total))) %>%
# keep only rows with frequency > 0 and present_yes_1 = 1
filter(!is.na(frequency_tally_total) & frequency_tally_total > 0) %>%
filter(present_yes_1 != 0)
summary_totals <- metadata_all %>%
# clean up names and formats
mutate(
date_collected = as_date(date_collected),
indicator_type = str_to_lower(str_squish(indicator_type)) # make categories consistent
) %>%
# group by researcher, date, location, and indicator type
group_by(location, date_collected, researcher, indicator_type) %>%
summarise(
total_frequency = sum(frequency_tally_total, na.rm = TRUE),
.groups = "drop"
) %>%
# reshape to wide format so each category is a column
pivot_wider(
names_from = indicator_type,
values_from = total_frequency,
values_fill = 0
) %>%
arrange(location, date_collected, researcher)
# preview results
head(summary_totals)
## # A tibble: 6 × 6
## location date_collected researcher `physical disorder` positive social orde…¹
## <chr> <date> <chr> <dbl> <dbl>
## 1 Beltline… 2025-10-17 EM 94 1
## 2 Beltline… 2025-10-17 VG 120 27
## 3 Beltline… 2025-10-19 AD 117 14
## 4 Beltline… 2025-10-19 CD 124 20
## 5 Beltline… 2025-10-19 RL 127 28
## 6 Beltline… 2025-10-19 VG 136 27
## # ℹ abbreviated name: ¹`positive social order`
## # ℹ 1 more variable: `social disorder` <dbl>
metadata_all <- metadata_all %>%
mutate(location = if_else(is.na(location) | location == "", "Sunalta", location))
df <- summary_totals
df <- df %>%
rename(
physical_disorder = `physical disorder`,
positive_social_order = `positive social order`,
social_disorder = `social disorder`
)
summary_stats <- df %>%
group_by(location) %>%
summarise(across(c(physical_disorder, positive_social_order, social_disorder),
list(mean = mean, sd = sd, median = median), .names = "{col}_{fn}"))
summary_stats
## # A tibble: 3 × 10
## location physical_disorder_mean physical_disorder_sd physical_disorder_me…¹
## <chr> <dbl> <dbl> <dbl>
## 1 Beltline S… 121. 16.4 122
## 2 Sunalta 69.9 20.6 77
## 3 <NA> 0 0 0
## # ℹ abbreviated name: ¹physical_disorder_median
## # ℹ 6 more variables: positive_social_order_mean <dbl>,
## # positive_social_order_sd <dbl>, positive_social_order_median <dbl>,
## # social_disorder_mean <dbl>, social_disorder_sd <dbl>,
## # social_disorder_median <dbl>
# convert data to long format for plotting
df_long <- df %>%
pivot_longer(
cols = c(physical_disorder, positive_social_order, social_disorder),
names_to = "indicator",
values_to = "count"
)
# boxplot: counts per indicator, colored by researcher, faceted by location
ggplot(df_long, aes(x = researcher, y = count, fill = researcher)) +
geom_boxplot(alpha = 0.7) +
geom_jitter(width = 0.1, alpha = 0.5) + # show individual points
facet_wrap(~ indicator + location, scales = "free_y") +
labs(
title = "Distribution of Observations per Indicator and Researcher",
x = "Researcher",
y = "Count"
) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "none"
)

df %>%
group_by(location, date_collected) %>%
summarise(across(c(physical_disorder, positive_social_order, social_disorder), sum)) %>%
pivot_longer(cols = c(physical_disorder, positive_social_order, social_disorder),
names_to = "indicator", values_to = "total") %>%
ggplot(aes(x = date_collected, y = total, fill = indicator)) +
geom_bar(stat = "identity", position = "dodge") +
facet_wrap(~location) +
theme_minimal() +
labs(title = "Daily Indicator Counts by Location")
## `summarise()` has grouped output by 'location'. You can override using the
## `.groups` argument.

df %>%
group_by(location, date_collected) %>%
summarise(across(c(physical_disorder, positive_social_order, social_disorder), sum)) %>%
pivot_longer(cols = c(physical_disorder, positive_social_order, social_disorder),
names_to = "indicator", values_to = "total") %>%
ggplot(aes(x = as.Date(date_collected, format="%m/%d/%y"), y = total, color = indicator, group = indicator)) +
geom_line() +
geom_point() +
facet_wrap(~location) +
theme_minimal() +
labs(title = "Trend of Observations Over Time")
## `summarise()` has grouped output by 'location'. You can override using the
## `.groups` argument.

# example: for 'physical disorder'
sunalta <- df %>% filter(location == "Sunalta") %>% pull(physical_disorder)
chumir <- df %>% filter(grepl("Beltline Sheldon Chumir", location)) %>% pull(physical_disorder)
effect_size <- (mean(sunalta) - mean(chumir)) / sd(c(sunalta, chumir))
pwr_result_pd <- pwr.t.test(d = effect_size, n = length(sunalta), sig.level = 0.05, type = "two.sample", alternative = "two.sided")
pwr_result_pd
##
## Two-sample t test power calculation
##
## n = 10
## d = 1.598664
## sig.level = 0.05
## power = 0.9219632
## alternative = two.sided
##
## NOTE: n is number in *each* group
# example: for 'physical disorder'
sunalta <- df %>% filter(location == "Sunalta") %>% pull(social_disorder)
chumir <- df %>% filter(grepl("Beltline Sheldon Chumir", location)) %>% pull(social_disorder)
effect_size <- (mean(sunalta) - mean(chumir)) / sd(c(sunalta, chumir))
pwr_result_sd <- pwr.t.test(d = effect_size, n = length(sunalta), sig.level = 0.05, type = "two.sample", alternative = "two.sided")
pwr_result_sd
##
## Two-sample t test power calculation
##
## n = 10
## d = 1.238313
## sig.level = 0.05
## power = 0.7450286
## alternative = two.sided
##
## NOTE: n is number in *each* group
# current effect size
d <- 1.227779
# calculate required n per group for power = 0.8
required_n_sd <- pwr.t.test(d = d, power = 0.8, sig.level = 0.05, type = "two.sample", alternative = "two.sided")
required_n_sd
##
## Two-sample t test power calculation
##
## n = 11.45818
## d = 1.227779
## sig.level = 0.05
## power = 0.8
## alternative = two.sided
##
## NOTE: n is number in *each* group
# example: for 'physical disorder'
sunalta <- df %>% filter(location == "Sunalta") %>% pull(positive_social_order)
chumir <- df %>% filter(grepl("Beltline Sheldon Chumir", location)) %>% pull(positive_social_order)
effect_size <- (mean(sunalta) - mean(chumir)) / sd(c(sunalta, chumir))
pwr_result_pso <- pwr.t.test(d = effect_size, n = length(sunalta), sig.level = 0.05, type = "two.sample", alternative = "two.sided")
pwr_result_pso
##
## Two-sample t test power calculation
##
## n = 10
## d = 1.066858
## sig.level = 0.05
## power = 0.6168342
## alternative = two.sided
##
## NOTE: n is number in *each* group
# current effect size
d <- 0.9069098
# calculate required n per group for power = 0.8
required_n_pso <- pwr.t.test(d = d, power = 0.8, sig.level = 0.05, type = "two.sample", alternative = "two.sided")
required_n_pso
##
## Two-sample t test power calculation
##
## n = 20.09283
## d = 0.9069098
## sig.level = 0.05
## power = 0.8
## alternative = two.sided
##
## NOTE: n is number in *each* group
icc_data <- df %>%
mutate(subject = paste0(ifelse(grepl("Sunalta", location), "Sunalta", "Chumir"), " ", date_collected)) %>%
select(subject, researcher, physical_disorder, positive_social_order, social_disorder)
# View result
icc_data
## # A tibble: 26 × 5
## subject researcher physical_disorder positive_social_order social_disorder
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Chumir 20… EM 94 1 32
## 2 Chumir 20… VG 120 27 31
## 3 Chumir 20… AD 117 14 68
## 4 Chumir 20… CD 124 20 57
## 5 Chumir 20… RL 127 28 17
## 6 Chumir 20… VG 136 27 16
## 7 Chumir 20… AK 103 23 15
## 8 Chumir 20… EM 151 33 15
## 9 Chumir 20… EM 108 33 22
## 10 Chumir 20… VG 126 0 0
## # ℹ 16 more rows
# ICC(2,1) stands for Model 2, Type 1
# It measures the consistency or absolute agreement between raters when each target (in our case a
# location) is rated by the same set of raters, and those raters are
# considered a random sample from a larger population of possible raters
manual_icc2 <- function(df, measure){
df_var <- df %>%
dplyr::select(subject, researcher, all_of(measure)) %>%
dplyr::mutate(target = as.character(subject)) %>%
dplyr::select(target, researcher, all_of(measure))
df_var[[measure]] <- as.numeric(as.character(df_var[[measure]]))
# handle duplicates by averaging
df_wide <- df_var %>%
dplyr::group_by(target, researcher) %>%
dplyr::summarise(value = mean(.data[[measure]], na.rm = TRUE), .groups = "drop") %>%
tidyr::pivot_wider(names_from = researcher, values_from = value)
mat <- as.matrix(df_wide[,-1])
mode(mat) <- "numeric"
n_targets <- nrow(mat)
n_raters <- ncol(mat)
target_mean <- rowMeans(mat, na.rm = TRUE)
rater_mean <- colMeans(mat, na.rm = TRUE)
grand_mean <- mean(mat, na.rm = TRUE)
n_i <- rowSums(!is.na(mat))
ss_targets <- sum(n_i * (target_mean - grand_mean)^2)
n_j <- colSums(!is.na(mat))
ss_raters <- sum(n_j * (rater_mean - grand_mean)^2)
ss_residual <- 0
for(i in 1:n_targets){
for(j in 1:n_raters){
if(!is.na(mat[i,j])){
ss_residual <- ss_residual + (mat[i,j] - target_mean[i] - rater_mean[j] + grand_mean)^2
}
}
}
ms_targets <- ss_targets / (n_targets - 1)
ms_raters <- ss_raters / (n_raters - 1)
ms_residual <- ss_residual / ((n_targets - 1)*(n_raters - 1))
icc <- (ms_targets - ms_residual) /
(ms_targets + (n_raters - 1)*ms_residual + (n_raters/n_targets)*(ms_raters - ms_residual))
return(list(ICC = icc,
MS_target = ms_targets,
MS_rater = ms_raters,
MS_residual = ms_residual))
}
df <- icc_data
variables <- c("physical_disorder", "positive_social_order", "social_disorder")
df[variables] <- lapply(df[variables], function(x) as.numeric(as.character(x)))
icc_results <- lapply(variables, function(v){
manual_icc2(df, v)
})
names(icc_results) <- variables
icc_results
## $physical_disorder
## $physical_disorder$ICC
## EM
## 0.5483627
##
## $physical_disorder$MS_target
## [1] 1575.15
##
## $physical_disorder$MS_rater
## [1] 769.2225
##
## $physical_disorder$MS_residual
## EM
## 118.6104
##
##
## $positive_social_order
## $positive_social_order$ICC
## EM
## 0.5755499
##
## $positive_social_order$MS_target
## [1] 106.1357
##
## $positive_social_order$MS_rater
## [1] 24.66917
##
## $positive_social_order$MS_residual
## EM
## 9.9825
##
##
## $social_disorder
## $social_disorder$ICC
## EM
## 0.36733
##
## $social_disorder$MS_target
## [1] 422.3054
##
## $social_disorder$MS_rater
## [1] 454.0067
##
## $social_disorder$MS_residual
## EM
## 55.48369
ICC(2,1) Results Interpretation:
Inter-rater reliability was assessed using ICC(2,1), which evaluates
absolute agreement
between researchers for each observational measure.
Physical disorder showed moderate reliability (ICC = 0.55),
indicating a fair-to-good level
of agreement among raters.
Positive social order demonstrated slightly higher reliability (ICC
= 0.58), suggesting
consistent scoring across observers.
Social disorder had a lower ICC (0.37), reflecting weaker agreement
and suggesting that
this construct may require clearer rating criteria or additional
rater training.
Overall, reliability ranged from fair to good across domains, with
stronger consistency
observed for positive and physical order indicators.
Social disorder had a lower ICC (0.37), reflecting weaker agreement and suggesting that