library(MASS)
library(pracma)
library(tidyverse)
library(patchwork)
library(gt)
library(furrr)
plan(multisession, workers = availableCores() - 1)Outlier Exploration
1 Setup
1.1 Packages
1.2 Data
val_df <- read_rds("valence_tidy.rds")
hol_df <- read_rds("holistic_tidy.rds")2 Synchrony (Dynamic ISC)
To detect participants who may be disengaged or watching “a different movie” than the rest of the sample, we calculate Inter-Subject Correlation (ISC). For each participant, we calculate the correlation between their dynamic rating time-series and the average time-series of all other participants (leave-one-out average).
- Metric: Mean Pearson correlation (Fisher Z-transformed).
- Variable Name:
D_ISC(Dynamic ISC). - Threshold: \(r < 0.30\) (Flag for review).
# Calculate Leave-One-Out correlations for a single video's data
calc_video_isc <- function(video_data) {
# 1. Pivot to wide format
wide_mat <- video_data |>
select(Rater, Second, Rating) |>
pivot_wider(names_from = Rater, values_from = Rating, values_fill = NA) |>
select(-Second) |>
as.matrix()
n_raters <- ncol(wide_mat)
rater_ids <- colnames(wide_mat)
correlations <- numeric(n_raters)
for (i in 1:n_raters) {
# 2. Target rater vector
target_vec <- wide_mat[, i]
# 3. "Gold Standard" (mean of everyone else)
others_mean <- rowMeans(wide_mat[, -i], na.rm = TRUE)
# 4. Safety Checks
# Check if we have enough overlapping data points (>= 3) to correlate
valid_overlap <- sum(!is.na(target_vec) & !is.na(others_mean))
# Check for flat-lines (SD=0) which break correlation
# We only check SD on the *overlapping* segments
is_flat <- function(x) {
v <- na.omit(x)
if(length(v) < 2) return(TRUE)
return(sd(v) == 0)
}
if (valid_overlap < 3 || is_flat(target_vec) || is_flat(others_mean)) {
correlations[i] <- NA
} else {
# 5. Pearson correlation with pairwise deletion
cor_val <- suppressWarnings(
cor(target_vec, others_mean, use = "pairwise.complete.obs")
)
correlations[i] <- cor_val
}
}
tibble(Rater = as.integer(rater_ids), r_val = correlations)
}# Process ISC: Nest by video, map function, then average per Rater
rater_isc_summary <-
val_df |>
nest(.by = Abbrev) |>
mutate(isc_data = map(data, calc_video_isc)) |>
select(Abbrev, isc_data) |>
unnest(isc_data) |>
mutate(z_val = 0.5 * log((1 + r_val) / (1 - r_val))) |> # Fisher Z
summarize(
D_ISC_Z = mean(z_val, na.rm = TRUE),
.by = Rater
) |>
mutate(
# Transform back for interpretable thresholding
D_ISC = (exp(2 * D_ISC_Z) - 1) / (exp(2 * D_ISC_Z) + 1)
)The histogram below visualizes the distribution of synchrony. Participants flagged in red fall below the 0.30 threshold.
bin_w <- 0.05
outliers <- rater_isc_summary |>
filter(D_ISC < 0.3) |>
mutate(
bin_center = floor(D_ISC / bin_w) * bin_w + (bin_w / 2)
)
ggplot(rater_isc_summary, aes(x = D_ISC)) +
geom_histogram(
color = "white",
fill = "steelblue",
breaks = seq(0, 1, by = bin_w)
) +
geom_vline(xintercept = 0.3, linetype = "dashed", color = "red") +
geom_text(
data = outliers,
aes(x = bin_center, y = 1, label = Rater),
vjust = -0.5,
color = "red",
fontface = "bold"
) +
scale_x_continuous(
limits = c(0, 1),
breaks = seq(0, 1, by = bin_w*2)
) +
scale_y_continuous(limits = c(0, 20), expand = c(0, 0)) +
labs(
x = "Mean Inter-Subject Correlation (Dynamic)",
y = "Count (Participants)"
) +
theme_classic()3 Complexity (Dynamic ENT)
To detect participants who are either “flat-lining” (too smooth) or “randomly clicking” (too jagged), we calculate the Sample Entropy of their ratings.
We normalize this metric using robust Z-scores (Median/MAD) on the Log-Transformed entropy values to account for the floor effect at zero.
- Metric: Sample Entropy (Log-Transformed).
- Variable Name:
D_ENT(Dynamic Entropy). - Threshold: \(Z < -2.5\) (Flat-liner) or \(Z > 2.5\) (Random Clicker).
calc_entropy_safe <- function(rating_vec) {
clean_vec <- as.numeric(as.vector(na.omit(rating_vec)))
if (length(clean_vec) < 3) {
return(NA_real_)
}
if (sd(clean_vec) == 0) {
return(0)
}
tryCatch(
{
val <- sample_entropy(clean_vec, edim = 2, r = 0.2 * sd(clean_vec))
if (is.infinite(val) || is.nan(val)) {
return(NA_real_)
}
return(as.numeric(val))
},
error = function(e) {
warning(paste("Entropy Failed:", e$message))
return(NA_real_)
}
)
}# Process Entropy (Complexity)
rater_entropy_summary <- val_df |>
nest(.by = c(Rater, Abbrev)) |>
mutate(
Samp_Entropy = future_map_dbl(
data,
~ calc_entropy_safe(.x$Rating),
.options = furrr_options(seed = TRUE)
)
) |>
select(-data) |>
summarize(
Mean_Entropy = mean(Samp_Entropy, na.rm = TRUE),
.by = Rater
) |>
mutate(
D_ENT = log(Mean_Entropy + 1e-6),
D_ENT_Z = (D_ENT - median(D_ENT, na.rm = TRUE)) / mad(D_ENT, na.rm = TRUE)
)The histogram below shows the distribution of entropy. Outliers on the far left represent unusually static ratings, while outliers on the far right represent unusually chaotic ratings.
bin_w <- 0.25
outliers <-
rater_entropy_summary |>
filter(abs(D_ENT_Z) > 2.5) |>
mutate(bin_center = floor(D_ENT_Z / bin_w) * bin_w + (bin_w / 2)) |>
mutate(
.by = bin_center,
stack_rank = row_number(),
y_stacked = 1 + max(stack_rank) + (stack_rank - 1)
)
ggplot(rater_entropy_summary, aes(x = D_ENT_Z)) +
geom_histogram(
color = "white",
fill = "steelblue",
breaks = seq(-5, 5, by = bin_w)
) +
geom_vline(xintercept = c(-2.5, 2.5), linetype = "dashed", color = "red") +
geom_text(
data = outliers,
aes(x = bin_center, y = y_stacked, label = Rater),
size = 3,
color = "red",
fontface = "bold"
) +
labs(
x = "Standardized Log Entropy (Dynamic)",
y = "Count (Participants)"
) +
theme_classic()4 Intensity (Dynamic VAL)
To detect participants who are rating the videos with unusually positive or negative bias on average across the time-series, we calculate the Mean Dynamic Valence. We normalize this using Robust Z-scores.
- Metric: Robust Z-Score (Median/MAD) of the mean dynamic rating.
- Variable Name:
D_VAL(Dynamic Valence). - Threshold: \(Z > 2.5\) (Extreme Positive Bias) or \(Z < -2.5\) (Extreme Negative Bias).
# Process Dynamic Valence
rater_val_summary <- val_df |>
summarize(
D_VAL = mean(Rating, na.rm = TRUE),
.by = Rater
) |>
mutate(
# Robust Z-score
D_VAL_Z = (D_VAL - median(D_VAL, na.rm = TRUE)) / mad(D_VAL, na.rm = TRUE)
)The histogram below shows the distribution of entropy. Outliers on the far left represent unusually static ratings, while outliers on the far right represent unusually chaotic ratings.
bin_w <- 0.5
outliers <-
rater_val_summary |>
filter(abs(D_VAL_Z) > 2.5) |>
mutate(bin_center = floor(D_VAL_Z / bin_w) * bin_w + (bin_w / 2)) |>
mutate(
.by = bin_center,
stack_rank = row_number(),
y_stacked = 1 + max(stack_rank) + (stack_rank - 1) * 1.25
)
ggplot(rater_val_summary, aes(x = D_VAL_Z)) +
geom_histogram(
color = "white",
fill = "steelblue",
breaks = seq(-5, 5, by = bin_w)
) +
geom_vline(xintercept = c(-2.5, 2.5), linetype = "dashed", color = "red") +
geom_text(
data = outliers,
aes(x = bin_center, y = y_stacked, label = Rater),
size = 3,
color = "red",
fontface = "bold"
) +
labs(
x = "Standardized Valence Mean (Dynamic)",
y = "Count (Participants)"
) +
theme_classic()5 Intensity (Holistic POS/NEG)
To detect participants who are consistently rating unnaturally high or low on the holistic scales, we calculate Robust Z-Scores for both Positive and Negative affect separately.
- Metric: Robust Z-Score (Median/MAD).
- Variable Names:
H_POS,H_NEG. - Threshold: \(Z > 2.5\) (Extreme Intensity) or \(Z < -2.5\) (Unusually Low).
# Aggregate holistic data
rater_hol_summary <-
hol_df |>
summarize(
Mean_Score = mean(Rating, na.rm = TRUE),
.by = c(Rater, Scale)
) |>
pivot_wider(names_from = Scale, values_from = Mean_Score) |>
rename(H_POS = Positive, H_NEG = Negative) |>
mutate(
# Calculate Robust Z-scores for each scale separately
H_POS_Z = (H_POS - median(H_POS, na.rm = TRUE)) / mad(H_POS, na.rm = TRUE),
H_NEG_Z = (H_NEG - median(H_NEG, na.rm = TRUE)) / mad(H_NEG, na.rm = TRUE)
)The plots below show the distribution of Positive and Negative intensity.
# Function to plot univariate Z-scores
plot_intensity <- function(data, column, label) {
bin_w <- 0.5
col_sym <- rlang::ensym(column)
break_seq <- seq(-5, 5, by = bin_w)
outliers <- data |>
filter(abs(!!col_sym) > 2.5) |>
mutate(bin_center = floor(!!col_sym / bin_w) * bin_w + (bin_w / 2)) |>
arrange(bin_center, Rater) |>
mutate(
stack_rank = row_number(),
y_stacked = 1 + max(stack_rank) + (stack_rank - 1) * 2,
.by = bin_center
)
ggplot(data, aes(x = !!col_sym)) +
# Use breaks instead of binwidth
geom_histogram(color = "white", fill = "steelblue", breaks = break_seq) +
geom_vline(xintercept = c(-2.5, 2.5), linetype = "dashed", color = "red") +
geom_text(
data = outliers,
aes(x = bin_center, y = y_stacked, label = Rater),
size = 3.25, color = "red", fontface = "bold"
) +
labs(x = label, y = "Count") +
theme_classic()
}
p1 <- plot_intensity(rater_hol_summary, H_POS_Z, "Standardized Positive Affect (Holistic)")
p2 <- plot_intensity(rater_hol_summary, H_NEG_Z, "Standardized Negative Affect (Holistic)")
p1 / p26 Final Integration
We combine all metrics into a single quality control table using standardized variable names:
- D_ISC_Z: Dynamic Inter-Subject Correlation (Fisher Z)
- D_ENT_Z: Dynamic Entropy (Log-transformed Z)
- D_VAL_Z: Dynamic Valence (Mean Z)
- H_POS_Z: Holistic Positive (Mean Z)
- H_NEG_Z: Holistic Negative (Mean Z)
6.1 Correlation Check
# 1. Join and Standardize
final_qc_prep <-
rater_isc_summary |>
left_join(rater_entropy_summary, by = "Rater") |>
left_join(rater_val_summary, by = "Rater") |>
left_join(rater_hol_summary, by = "Rater") |>
transmute(
Rater,
D_ISC_Z, # Fisher Z
D_ENT_Z, # Log Robust Z
D_VAL_Z, # Mean Robust Z
H_POS_Z, # Robust Z
H_NEG_Z, # Robust Z
D_ISC,
D_ENT,
D_VAL,
H_POS,
H_NEG
) |>
drop_na()
# 2. Correlation Matrix
cor_matrix <-
final_qc_prep |>
select(ends_with("_Z")) |>
cor()
# Display Correlations
cor_matrix |>
as.data.frame() |>
rownames_to_column("Metric") |>
gt() |>
fmt_number(columns = -Metric, decimals = 2) |>
tab_header(title = "Correlation of QC Metrics")| Correlation of QC Metrics | |||||
| Metric | D_ISC_Z | D_ENT_Z | D_VAL_Z | H_POS_Z | H_NEG_Z |
|---|---|---|---|---|---|
| D_ISC_Z | 1.00 | −0.06 | −0.25 | −0.09 | 0.02 |
| D_ENT_Z | −0.06 | 1.00 | 0.00 | 0.13 | 0.26 |
| D_VAL_Z | −0.25 | 0.00 | 1.00 | 0.40 | −0.37 |
| H_POS_Z | −0.09 | 0.13 | 0.40 | 1.00 | 0.37 |
| H_NEG_Z | 0.02 | 0.26 | −0.37 | 0.37 | 1.00 |
6.2 Global Outlier Detection
We calculate the Global Mahalanobis Distance.
- Pink cells indicate a potential failure.
- Green cells indicate passing values.
# Define variables for MCD
mcd_vars <- c("D_ISC_Z", "D_ENT_Z", "D_VAL_Z", "H_POS_Z", "H_NEG_Z")
# 4. Calculate MCD and Distances
mcd_global <-
final_qc_prep |>
select(all_of(mcd_vars)) |>
cov.mcd()
final_qc <-
final_qc_prep |>
mutate(
Global_Dist = mahalanobis(
x = pick(all_of(mcd_vars)),
center = mcd_global$center,
cov = mcd_global$cov
),
Global_pval = pchisq(Global_Dist, df = length(mcd_vars), lower.tail = FALSE),
# Flags
Global_MD = Global_pval < .001,
Low_ISC = D_ISC < 0.3,
Low_ENT = D_ENT_Z < -2.5,
High_ENT = D_ENT_Z > 2.5,
Low_VAL = D_VAL_Z < -2.5,
High_VAL = D_VAL_Z > 2.5,
Low_H_POS = H_POS_Z < -2.5,
High_H_POS = H_POS_Z > 2.5,
Low_H_NEG = H_NEG_Z < -2.5,
High_H_NEG = H_NEG_Z > 2.5,
# Count Total Flags
Flag_Count = rowSums(pick(Global_MD, Low_ISC, Low_ENT, High_ENT,
Low_VAL, High_VAL,
Low_H_POS, High_H_POS, Low_H_NEG, High_H_NEG))
)# 5. Final Output Table
final_qc |>
filter(Flag_Count > 0) |>
relocate(Flag_Count, .after = Rater) |>
arrange(Rater) |>
select(
Rater, Flag_Count, Global_MD,
Low_ISC, Low_ENT, High_ENT,
Low_VAL, High_VAL,
Low_H_POS, High_H_POS, Low_H_NEG, High_H_NEG
) |>
gt() |>
data_color(
columns = where(is.logical),
fn = function(x) ifelse(x == TRUE, "#ffcccc", "#ccffcc")
) |>
tab_header(
title = "Quality Control Summary",
subtitle = "Flags for Synchrony, Complexity, Intensity, and Incoherence"
)| Quality Control Summary | |||||||||||
| Flags for Synchrony, Complexity, Intensity, and Incoherence | |||||||||||
| Rater | Flag_Count | Global_MD | Low_ISC | Low_ENT | High_ENT | Low_VAL | High_VAL | Low_H_POS | High_H_POS | Low_H_NEG | High_H_NEG |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 17 | 3 | TRUE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE | TRUE | FALSE | FALSE |
| 29 | 1 | FALSE | FALSE | FALSE | TRUE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE |
| 40 | 1 | FALSE | TRUE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE |
| 57 | 1 | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE |
| 58 | 1 | TRUE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE |
| 59 | 1 | FALSE | FALSE | TRUE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE |
| 67 | 1 | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE | FALSE |
| 80 | 1 | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE | FALSE |
| 81 | 4 | TRUE | TRUE | FALSE | FALSE | TRUE | FALSE | FALSE | FALSE | FALSE | TRUE |
| 99 | 1 | FALSE | FALSE | FALSE | TRUE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE |
7 Normative Baselines
To establish a reference standard for future data collection, we calculate the normative parameters (Median and MAD) and the multivariate covariance structure of the current sample. Crucially, we calculate these norms using only the “Valid” subset of participants (i.e., excluding those flagged for low synchrony, flat-lining, or data incoherence). This ensures our baseline represents “attentive human performance” without noise contamination.
# 1. Define the "Valid" Reference Set
# We exclude anyone with a QC flag to prevent outliers from contaminating the norms
valid_raters <- final_qc |>
filter(Flag_Count == 0) |>
select(Rater)
clean_data <- final_qc_prep |>
semi_join(valid_raters, by = "Rater")
# 2. Calculate Univariate Norms (on Clean Data Only)
normative_stats <- clean_data |>
summarize(
# Synchrony (Fisher Z)
D_ISC_Median = median(D_ISC, na.rm = TRUE),
D_ISC_MAD = mad(D_ISC, na.rm = TRUE),
# Complexity (Log Entropy)
D_ENT_Median = median(D_ENT, na.rm = TRUE),
D_ENT_MAD = mad(D_ENT, na.rm = TRUE),
# Dynamic Valence
D_VAL_Median = median(D_VAL, na.rm = TRUE),
D_VAL_MAD = mad(D_VAL, na.rm = TRUE),
# Intensity Positive
H_POS_Median = median(H_POS, na.rm = TRUE),
H_POS_MAD = mad(H_POS, na.rm = TRUE),
# Intensity Negative
H_NEG_Median = median(H_NEG, na.rm = TRUE),
H_NEG_MAD = mad(H_NEG, na.rm = TRUE)
) |>
pivot_longer(
everything(),
names_to = "Temp",
values_to = "Value"
) |>
mutate(
Stat = str_extract(Temp, "(Median|MAD)$"),
Metric = str_remove(Temp, "_(Median|MAD)$")
) |>
select(-Temp) |>
pivot_wider(names_from = Stat, values_from = Value)
# 3. Save & Display
write_rds(normative_stats, "normative_univariate_stats.rds")
normative_stats |>
gt() |>
tab_header(
title = "Normative Parameters (Healthy Reference)",
subtitle = "Derived from valid participant subset"
) |>
fmt_number(columns = c(Median, MAD), decimals = 3) |>
cols_label(
Metric = "QC Metric",
Median = "Reference Median",
MAD = "Reference MAD"
)| Normative Parameters (Healthy Reference) | ||
| Derived from valid participant subset | ||
| QC Metric | Reference Median | Reference MAD |
|---|---|---|
| D_ISC | 0.722 | 0.126 |
| D_ENT | −2.419 | 0.497 |
| D_VAL | −0.025 | 0.437 |
| H_POS | 1.181 | 0.416 |
| H_NEG | 0.771 | 0.421 |
# 4. Calculate Multivariate Norms (MCD on Clean Data)
# This provides the reference 'Cloud' for future Mahalanobis Distance checks
mcd_clean <-
clean_data |>
select(D_ISC, D_ENT, D_VAL, H_POS, H_NEG) |>
cov.mcd()
multivariate_norms <- list(
center = mcd_clean$center,
cov = mcd_clean$cov
)
write_rds(multivariate_norms, "normative_multivariate_stats.rds")
# 5. Display Reference Correlation Matrix
# This confirms the relationships in the healthy baseline
cov2cor(mcd_clean$cov) |>
as.data.frame() |>
rownames_to_column("Metric") |>
gt() |>
tab_header(
title = "Reference Correlation Structure",
subtitle = "Correlations among QC metrics in valid sample"
) |>
fmt_number(columns = -Metric, decimals = 2)| Reference Correlation Structure | |||||
| Correlations among QC metrics in valid sample | |||||
| Metric | D_ISC | D_ENT | D_VAL | H_POS | H_NEG |
|---|---|---|---|---|---|
| D_ISC | 1.00 | −0.04 | −0.23 | −0.02 | 0.19 |
| D_ENT | −0.04 | 1.00 | 0.26 | 0.30 | 0.23 |
| D_VAL | −0.23 | 0.26 | 1.00 | 0.35 | −0.29 |
| H_POS | −0.02 | 0.30 | 0.35 | 1.00 | 0.48 |
| H_NEG | 0.19 | 0.23 | −0.29 | 0.48 | 1.00 |
8 Strategic Selection for Clinical Discriminability
To optimize the detection of negative affective bias (a transdiagnostic marker of emotional disorders), stimulus selection must account for the psychometric properties of the healthy normative sample. Specifically, we aim to avoid ceiling effects that obscure clinical elevations while ensuring discriminant validity between appropriate and inappropriate negative responding. We stratify the stimuli into three functional categories based on their intensity and variance in the healthy sample:
- Ambiguous/Moderate Probes (Primary Diagnostic Utility)
- Definition: Stimuli eliciting mild-to-moderate negative affect (\(M \approx 1.0-2.0\)) with high inter-individual variance (\(SD > 0.8\)).
- Rationale: These stimuli possess the greatest discriminative power for negative bias. Because healthy controls do not rate these at the scale maximum, they provide sufficient psychometric “headroom” to detect hypersensitivity. Elevated ratings in this category reflect a tendency to interpret ambiguous or moderately negative cues as severe.
- High-Intensity Provocation (Reactivity & Regulation)
- Definition: Stimuli eliciting near-maximal negative affect (\(M > 2.5\)) in the healthy sample.
- Rationale: While these stimuli may suffer from ceiling effects regarding peak reactivity (limiting their ability to distinguish patients from controls on intensity alone), they remain essential for assessing emotion regulation and recovery. They serve as a “stress test” to observe the duration of the affective response post-stimulus.
- Low-Intensity Controls (Specificity)
- Definition: Stimuli perceived as neutral or minimally negative (\(M < 0.8\)) by healthy controls.
- Rationale: These stimuli establish the specificity of the negative bias. High negative ratings in this category suggest a decoupling of affect from context (e.g., hostile attribution bias or generalized negative affectivity) rather than a potentiated response to actual threat.
Table Interpretation:
- Healthy_Mean: The normative baseline for negative affect. Lower values indicate greater capacity to detect clinical elevations.
- Bias_Headroom: The remaining range on the Likert scale (Max - Healthy Mean). Larger values indicate greater sensitivity to exaggerated responses.
- Clinical_Role: The proposed functional classification for the stimulus in a clinical battery.
# -------------------------------------------------------------------------
# Clinical Stimulus Selection (Negative Bias Focus)
# -------------------------------------------------------------------------
# 1. Calculate Reliability (Mean ISC per Video)
video_reliability <- val_df |>
nest(.by = Abbrev) |>
mutate(isc_data = map(data, calc_video_isc)) |>
select(Abbrev, isc_data) |>
unnest(isc_data) |>
summarize(
Reliability_ISC = mean(r_val, na.rm = TRUE),
.by = Abbrev
)
# 2. Calculate Discriminability (SD of Holistic Negative Affect)
video_discriminability <- hol_df |>
filter(Scale == "Negative") |>
summarize(
Mean_Neg = mean(Rating, na.rm = TRUE),
SD_Neg = sd(Rating, na.rm = TRUE),
.by = Abbrev
)
# 3. Create Clinical Selection Table
clinical_selection <-
video_reliability |>
left_join(video_discriminability, by = "Abbrev") |>
mutate(
# --- RENAME FOR CLARITY ---
# Explicitly state this is Holistic Negative Affect
Mean_Holistic_NA = Mean_Neg,
SD_Holistic_NA = SD_Neg,
# 2. Assign Clinical Roles
Clinical_Role = case_when(
Reliability_ISC < 0.40 ~ "Exclusion (Low Reliability)",
Mean_Holistic_NA > 2.0 ~ "High-Intensity Provocation",
Mean_Holistic_NA >= 0.8 & Mean_Holistic_NA <= 2.0 & SD_Holistic_NA > 0.8 ~ "Ambiguous/Moderate Probe",
Mean_Holistic_NA < 0.8 ~ "Low-Intensity Control",
TRUE ~ "General Stimulus"
),
# 3. "Headroom" Score
Bias_Headroom = 4.0 - Mean_Holistic_NA
) |>
select(Abbrev, Clinical_Role, Reliability_ISC, Mean_Holistic_NA, SD_Holistic_NA, Bias_Headroom) |>
arrange(Clinical_Role, Abbrev)
# 4. The Clinical Selection Table
clinical_selection |>
gt() |>
tab_header(
title = "Stimulus Portfolio for Clinical Applications",
subtitle = "Stratification by Intensity and Discriminative Potential"
) |>
fmt_number(columns = where(is.numeric), decimals = 2) |>
# --- IMPROVED COLUMN LABELS ---
cols_label(
Reliability_ISC = "Reliability (ISC)",
Mean_Holistic_NA = "Mean Negative (Holistic)",
SD_Holistic_NA = "SD Negative (Holistic)",
Bias_Headroom = "Clinical Headroom"
) |>
# Color code the roles
data_color(
columns = Clinical_Role,
fn = function(x) {
colors <- c(
"Ambiguous/Moderate Probe" = "#cce5ff",
"High-Intensity Provocation" = "#ffe5cc",
"Low-Intensity Control" = "#e5ffcc",
"Exclusion (Low Reliability)" = "#ffcccc",
"General Stimulus" = "white"
)
colors[as.character(x)]
}
) |>
# Visualizing the Headroom
data_color(
columns = Bias_Headroom,
fn = scales::col_numeric(palette = c("white", "grey"), domain = NULL)
)| Stimulus Portfolio for Clinical Applications | |||||
| Stratification by Intensity and Discriminative Potential | |||||
| Abbrev | Clinical_Role | Reliability (ISC) | Mean Negative (Holistic) | SD Negative (Holistic) | Clinical Headroom |
|---|---|---|---|---|---|
| Fences | Ambiguous/Moderate Probe | 0.77 | 1.79 | 1.24 | 2.21 |
| GoodWill | Ambiguous/Moderate Probe | 0.61 | 0.97 | 0.99 | 3.03 |
| MarriageStory | Ambiguous/Moderate Probe | 0.69 | 1.51 | 1.27 | 2.49 |
| NoCountry | Ambiguous/Moderate Probe | 0.55 | 1.34 | 1.21 | 2.66 |
| SilenceLambs | Ambiguous/Moderate Probe | 0.62 | 1.59 | 1.16 | 2.41 |
| Zodiac | Ambiguous/Moderate Probe | 0.82 | 1.89 | 1.29 | 2.11 |
| DaysSummer | Exclusion (Low Reliability) | 0.37 | 0.43 | 0.75 | 3.57 |
| ForrestGump | Exclusion (Low Reliability) | 0.37 | 0.60 | 0.95 | 3.40 |
| LittleMiss | Exclusion (Low Reliability) | 0.32 | 0.57 | 0.89 | 3.43 |
| PulpFiction | Exclusion (Low Reliability) | 0.40 | 0.86 | 1.03 | 3.14 |
| GreenMile | High-Intensity Provocation | 0.68 | 2.19 | 1.39 | 1.81 |
| AkeelahBee | Low-Intensity Control | 0.86 | 0.37 | 0.76 | 3.63 |
| BealeStreet | Low-Intensity Control | 0.50 | 0.66 | 0.91 | 3.34 |
| CatchMe | Low-Intensity Control | 0.65 | 0.46 | 0.78 | 3.54 |
| KingsSpeech | Low-Intensity Control | 0.43 | 0.74 | 1.02 | 3.26 |
| LadyBird | Low-Intensity Control | 0.55 | 0.41 | 0.72 | 3.59 |
| LegallyBlonde | Low-Intensity Control | 0.57 | 0.26 | 0.54 | 3.74 |
| Miracle | Low-Intensity Control | 0.88 | 0.31 | 0.67 | 3.69 |
| Moonlight | Low-Intensity Control | 0.61 | 0.47 | 0.74 | 3.53 |
| ParentTrap | Low-Intensity Control | 0.56 | 0.25 | 0.62 | 3.75 |
| PursuitHappyness | Low-Intensity Control | 0.76 | 0.34 | 0.67 | 3.66 |
| SocialNetwork | Low-Intensity Control | 0.50 | 0.59 | 0.90 | 3.41 |