# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2025 SWGY, Inc
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#install.packages("dplyr")
#install.packages("tidyr")
library(tidyr)
library(dplyr)
df <- read.csv("data/blast-results-2025-04-19-2024.csv")
# Consider each blast location a "subject"
# Split the data set based on distance from the origin
df$distance <- sqrt(df$bp_x^2 + df$bp_y^2 + df$bp_z^2)
near_df <- subset(df, distance <= 2.5) # Close blasts
intermediate_df <- subset(df, distance >= 2.5) # Intermediate distance
# Pivot to "long" format so each row represents an armor condition
near_df$subject_id <- seq_len(nrow(near_df))
near_long <- pivot_longer(
near_df,
cols = c("full_armor", "helmet_only", "vest_only", "no_armor"),
names_to = "ArmorCondition",
values_to = "Impulse")
intermediate_df$subject_id <- seq_len(nrow(intermediate_df))
intermediate_long <- pivot_longer(
intermediate_df,
cols = c("full_armor", "helmet_only", "vest_only", "no_armor"),
names_to = "ArmorCondition",
values_to = "Impulse")
res_aov_near <- aov(
Impulse ~ ArmorCondition + Error(subject_id / ArmorCondition),
data = near_long)
summary(res_aov_near)
res_aov_intermediate <- aov(
Impulse ~ ArmorCondition + Error(subject_id / ArmorCondition),
data = intermediate_long)
summary(res_aov_intermediate)
# The intermediate data set shows armor condition is significant (p=1.26e-09, F=14.82)
# Let's explore more...
# This data set has only intermediate range blasts and more of them:
intermediate_df2 <- read.csv("data/blast-results-2025-04-21-1055.csv")
intermediate_df2$subject_id <- seq_len(nrow(intermediate_df2))
intermediate_long2 <- pivot_longer(
intermediate_df2,
cols = c("full_armor", "helmet_only", "vest_only", "no_armor"),
names_to = "ArmorCondition",
values_to = "Impulse")
intermediate_wide2 <- pivot_wider(
data = intermediate_long2,
id_cols = "subject_id",
names_from = "ArmorCondition",
values_from = "Impulse")
# 1) run and store each p‑value
p_full_helmet <- t.test(intermediate_wide2$full_armor, intermediate_wide2$helmet_only, paired=TRUE)$p.value
p_full_vest <- t.test(intermediate_wide2$full_armor, intermediate_wide2$vest_only, paired=TRUE)$p.value
p_full_none <- t.test(intermediate_wide2$full_armor, intermediate_wide2$no_armor, paired=TRUE)$p.value
p_helmet_vest <- t.test(intermediate_wide2$helmet_only, intermediate_wide2$vest_only, paired=TRUE)$p.value
p_helmet_none <- t.test(intermediate_wide2$helmet_only, intermediate_wide2$no_armor, paired=TRUE)$p.value
p_vest_none <- t.test(intermediate_wide2$vest_only, intermediate_wide2$no_armor, paired=TRUE)$p.value
# 2) combine into a named vector
pvals <- c(
`full vs helmet` = p_full_helmet,
`full vs vest` = p_full_vest,
`full vs none` = p_full_none,
`helmet vs vest` = p_helmet_vest,
`helmet vs none` = p_helmet_none,
`vest vs none` = p_vest_none
)
# 3) Holm-adjust
adj <- p.adjust(pvals, method = "holm")
# 4) print them individually
for(name in names(pvals)) {
cat(sprintf(
"%-15s raw p = %0.4g; Holm adj p = %0.4g\n",
name, pvals[name], adj[name]
))
}
# Calculate the specific impact of the helmet:
# mean impulse reduction (w/ SD and CI)
# mean attenuation (w/ interquartile)
# Cohen's D
# Distribution characterization
# 1) Compute absolute & percent reduction for each subject
wide2 <- intermediate_wide2 %>% mutate(
abs_reduction = no_armor - helmet_only,
pct_reduction = 100 * abs_reduction / no_armor
)
# 2) Number of subjects
n <- nrow(wide2)
# 3) Absolute reduction: mean, SD, SE, 95% CI
mean_abs <- mean(wide2$abs_reduction, na.rm = TRUE)
sd_abs <- sd(wide2$abs_reduction, na.rm = TRUE)
se_abs <- sd_abs / sqrt(n)
ci_abs <- mean_abs + c(-1,1) * qt(0.975, df = n-1) * se_abs
# 4) Percent reduction: mean, median, IQR
mean_pct <- mean(wide2$pct_reduction, na.rm = TRUE)
median_pct <- median(wide2$pct_reduction, na.rm = TRUE)
iqr_pct <- IQR(wide2$pct_reduction, na.rm = TRUE)
# 5) Cohen's d for the paired reduction (standardized effect)
# (Here we use the simple definition d = mean_abs / sd_abs)
cohen_d <- mean_abs / sd_abs
# 6) Fraction of blast points with ≥10% reduction
frac_10 <- mean(wide2$pct_reduction >= 10, na.rm = TRUE)
# 7) Print everything neatly
cat(sprintf(
"Absolute reduction: mean = %.3f (SD = %.3f), 95%% CI = [%.3f, %.3f]\n",
mean_abs, sd_abs, ci_abs[1], ci_abs[2]
))
cat(sprintf(
"Percent reduction: mean = %.1f%%, median = %.1f%%, IQR = %.1f%%–%.1f%%\n",
mean_pct, median_pct, quantile(wide2$pct_reduction, .25),
quantile(wide2$pct_reduction, .75)
))
cat(sprintf(
"Cohen's d (paired) = %.2f\n", cohen_d
))
cat(sprintf(
"Proportion with ≥10%% reduction = %.1f%%\n", 100 * frac_10
))
# calculate cohen's d on the percent reduction
wide2 <- intermediate_wide2 %>%
mutate(
pct_reduction = 100 * (no_armor - helmet_only) / no_armor
)
# 1) summary stats for percent reduction
mean_pct <- mean(wide2$pct_reduction, na.rm=TRUE)
sd_pct <- sd( wide2$pct_reduction, na.rm=TRUE)
# 2) Cohen’s d on percent reduction
cohen_d_pct <- mean_pct / sd_pct
# 3) print it out
cat(sprintf(
"Percent reduction: mean = %.1f%% (SD = %.1f%%) → Cohen's d = %.2f\n",
mean_pct, sd_pct, cohen_d_pct
))