if (!require("pacman")) install.packages("pacman", repos = "https://cloud.r-project.org")
pacman::p_load(
tidyverse,
jsonlite,
ggpubr,
psych
)
library(tidyverse)
library(jsonlite)
library(ggpubr)
library(pacman)
library(psych)
# Read the file line-by-line and parse each JSON row into a data frame
dat <- suppressMessages(
read_file("Pilot2_data.txt") %>%
str_split("\n") %>%
first() %>%
discard(~ .x == "" || .x == "\r") %>%
map_dfr(fromJSON, flatten = TRUE)
)
# Assign unique ID to each participant
dat$ID <- NA
tmp_IDcounter <- 0
for (i in 1:nrow(dat)) {
if (!is.na(dat$sender[i]) && dat$sender[i] == "Greetings") {
tmp_IDcounter <- tmp_IDcounter + 1
}
dat$ID[i] <- tmp_IDcounter
}
rm(tmp_IDcounter)
### ─────────────────────────────────────────────
### Calculate mean scores for Ecological Dimension
### ─────────────────────────────────────────────
# 1. Filter and select relevant columns
eco <- dat %>%
filter(sender == "Ecological Dimension Scale") %>%
select(ID, framingCondition, starts_with("EcologicalDimension"))
# 2. Convert char to numeric
eco[ , 3:ncol(eco)] <- lapply(eco[ , 3:ncol(eco)], function(x) as.numeric(trimws(x)))
# 3. Compute participant-level mean score
eco_scores <- eco
eco_scores$eco_mean <- rowMeans(eco_scores[, grep("EcologicalDimension", names(eco_scores))], na.rm = TRUE)
eco_scores <- eco_scores[, c("ID", "framingCondition", "eco_mean")]
### ─────────────────────────────────────────────
### Bioinspiration scale: Reverse Coding + Mean Scores
### ─────────────────────────────────────────────
# 1. Extract raw Bioinspiration item responses
bio_items_raw <- dat %>%
filter(sender == "Bioinspiration Scale") %>%
select(ID, starts_with("Bioinspiration"))
# 2. Convert ALL columns except ID to numeric
bio_items_raw[ , 2:ncol(bio_items_raw)] <- lapply(bio_items_raw[ , 2:ncol(bio_items_raw)], function(x) as.numeric(trimws(x)))
# 3. Compute participant-level mean BEFORE reverse coding
bio_mean_before <- rowMeans(bio_items_raw[, -1], na.rm = TRUE)
overall_mean_before <- mean(bio_mean_before, na.rm = TRUE)
# 4. Reverse-code the 3 negatively worded items — FIXED to preserve ID
bio_items_clean <- dat %>%
filter(sender == "Bioinspiration Scale") %>%
select(ID, starts_with("Bioinspiration")) %>%
mutate(
across(starts_with("Bioinspiration"), ~ as.numeric(trimws(as.character(.)))),
`Bioinspiration-IPI2r` = 6 - `Bioinspiration-IPI2r`,
`Bioinspiration-PN2r` = 6 - `Bioinspiration-PN2r`,
`Bioinspiration-VRtN4r` = 6 - `Bioinspiration-VRtN4r`
)
# 5. Compute participant-level mean AFTER reverse coding
bio_mean_after <- rowMeans(bio_items_clean[, -1], na.rm = TRUE)
overall_mean_after <- mean(bio_mean_after, na.rm = TRUE)
# 5b. Assemble cleaned scores table
bio_scores <- bio_items_clean %>%
mutate(bio_mean = bio_mean_after) %>%
select(ID, bio_mean)
# 6. Print summary
cat("Average mean BEFORE reverse coding:", round(overall_mean_before, 3), "\n")
## Average mean BEFORE reverse coding: 3.364
cat("Average mean AFTER reverse coding:", round(overall_mean_after, 3), "\n")
## Average mean AFTER reverse coding: 3.491
### ─────────────────────────────────────────────
### created framing info table
### ─────────────────────────────────────────────
# Extract from 'InformedConsent' page
frame_info <- dat %>%
filter(sender == "InformedConsent") %>%
select(ID, framingCondition)
# Merge true condition into score tables
eco_scores <- left_join(eco_scores, frame_info, by = "ID") %>%
select(-framingCondition.x) %>%
dplyr::rename(framingCondition = framingCondition.y)
bio_scores <- left_join(bio_scores, frame_info, by = "ID")
#number of pps in each framing condition
table(eco_scores$framingCondition, useNA = "always")
##
## bioinspired neutral sustainable <NA>
## 201 199 199 0
# Summary function
# Load required package
library(plyr)
## ------------------------------------------------------------------------------
## You have loaded plyr after dplyr - this is likely to cause problems.
## If you need functions from both plyr and dplyr, please load plyr first, then dplyr:
## library(plyr); library(dplyr)
## ------------------------------------------------------------------------------
##
## Attaching package: 'plyr'
## The following object is masked from 'package:ggpubr':
##
## mutate
## The following objects are masked from 'package:dplyr':
##
## arrange, count, desc, failwith, id, mutate, rename, summarise,
## summarize
## The following object is masked from 'package:purrr':
##
## compact
# Summary function
data_summary <- function(data, varname, groupnames){
summary_func <- function(x, col){
c(mean = mean(x[[col]], na.rm = TRUE),
se = sd(x[[col]], na.rm = TRUE) / sqrt(length(na.omit(x[[col]]))))
}
data_sum <- ddply(data, groupnames, .fun = summary_func, varname)
data_sum <- plyr::rename(data_sum, c("mean" = varname))
return(data_sum)
}
# Summaries of participant-level scores by framing condition
eco_summary <- data_summary(eco_scores, "eco_mean", "framingCondition")
bio_summary <- data_summary(bio_scores, "bio_mean", "framingCondition")
# View results
print(eco_summary)
## framingCondition eco_mean se
## 1 bioinspired 4.866915 0.07545508
## 2 neutral 5.472362 0.06765447
## 3 sustainable 5.869347 0.06002766
print(bio_summary)
## framingCondition bio_mean se
## 1 bioinspired 3.837479 0.03323343
## 2 neutral 3.458543 0.04533193
## 3 sustainable 3.174623 0.05264839
Sustainable framing leads to the highest ecological scores on average.
Sustainable framing produces the lowest bioinspiration scores.
Bioinspired framing leads to the highest bioinspiration scores.
Bioinspired framing results in the lowest ecological scores.
# Plots the mean scores on the Bioinspiration & Ecological Dimension by Framing Condition
ggplot(eco_scores, aes(x = framingCondition, y = eco_mean)) +
geom_boxplot(fill = "lightgreen") +
labs(title = "Ecological Dimension by Framing", y = "Mean Score") +
theme_minimal()
ggplot(bio_scores, aes(x = framingCondition, y = bio_mean)) +
geom_boxplot(fill = "skyblue") +
labs(title = "Bioinspiration by Framing", y = "Mean Score") +
theme_minimal()
# Violin plot for Ecological Dimension
ggplot(eco_scores, aes(x = framingCondition, y = eco_mean)) +
geom_violin(fill = "palegreen", alpha = 0.5, trim = FALSE) +
geom_jitter(width = 0.15, alpha = 0.4, color = "darkgreen") +
stat_summary(fun = mean, geom = "point", color = "black", size = 3) +
labs(title = "Ecological Scores by Framing", y = "Mean Score", x = "Framing Condition") +
theme_minimal()
# Violin plot for Bioinspiration
ggplot(bio_scores, aes(x = framingCondition, y = bio_mean)) +
geom_violin(fill = "lightblue", alpha = 0.5, trim = FALSE) +
geom_jitter(width = 0.15, alpha = 0.4, color = "darkblue") +
stat_summary(fun = mean, geom = "point", color = "black", size = 3) +
labs(title = "Bioinspiration Scores by Framing", y = "Mean Score", x = "Framing Condition") +
theme_minimal()
For the Ecological Items/Sustainability Scale:
# 1. Join pps bio inspiration and sustainability scores into one data frame
combined_scores <- left_join(bio_scores, eco_scores, by = c("ID", "framingCondition"))
#2. Run ANOVAs for spillover effects
#2a. Does framing affect Sustainability Scores?
eco_spillover <- aov(eco_mean ~ framingCondition, data = combined_scores)
summary(eco_spillover)
## Df Sum Sq Mean Sq F value Pr(>F)
## framingCondition 2 102.0 51.00 55.14 <2e-16 ***
## Residuals 596 551.2 0.92
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 2a.2. Post-hoc comparision
TukeyHSD(eco_spillover)
## Tukey multiple comparisons of means
## 95% family-wise confidence level
##
## Fit: aov(formula = eco_mean ~ framingCondition, data = combined_scores)
##
## $framingCondition
## diff lwr upr p adj
## neutral-bioinspired 0.6054464 0.3794879 0.8314048 0.0000000
## sustainable-bioinspired 1.0024313 0.7764729 1.2283898 0.0000000
## sustainable-neutral 0.3969849 0.1704623 0.6235076 0.0001291
Visualise the results for the Ecological Items/Sustainability Scale:
#Visualise Framing effect on ecological scores
ggplot(combined_scores, aes(x = framingCondition, y = eco_mean, fill = framingCondition)) +
geom_boxplot() +
labs(title = "Spillover: Framing Effect on Ecological Scores",
y = "Ecological Mean Score", x = "Framing Condition") +
theme_minimal()
For the Bioinspiration Scale:
# Does framing affect bioinspiration scores?”
# post-hoc Tukey
bio_spillover <- aov(bio_mean ~ framingCondition, data = combined_scores)
summary(bio_spillover)
## Df Sum Sq Mean Sq F value Pr(>F)
## framingCondition 2 44.26 22.129 56.22 <2e-16 ***
## Residuals 596 234.59 0.394
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
TukeyHSD(bio_spillover)
## Tukey multiple comparisons of means
## 95% family-wise confidence level
##
## Fit: aov(formula = bio_mean ~ framingCondition, data = combined_scores)
##
## $framingCondition
## diff lwr upr p adj
## neutral-bioinspired -0.3789366 -0.5263456 -0.2315276 0.00e+00
## sustainable-bioinspired -0.6628562 -0.8102652 -0.5154472 0.00e+00
## sustainable-neutral -0.2839196 -0.4316967 -0.1361425 2.28e-05
Visualise the results for the Bioinspiration Scale:
#Visualise Framing effect on bioinpspiration scores
ggplot(combined_scores, aes(x = framingCondition, y = bio_mean, fill = framingCondition)) +
geom_boxplot() +
labs(title = "Spillover: Framing Effect on Bioinspiration Scores",
y = "Bioinspiration Mean Score", x = "Framing Condition") +
theme_minimal()
Sustainable vignettes significantly boosted sustainability attitudes (suggests the vignettes are working), but interestingly, bioinspired framing scored lowest on this scale.
This shows strong domain specificity — and possibly a negative spillover??
Bioinspired vignettes significantly increased bioinspiration scale scores i.e., the vignettes are working.
But again, sustainable framing actually reduced bioinspiration, suggesting minimal or even negative spillover in the other direction.
# remove this awful package
remove.packages("plyr")
## Removing package from '/Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library'
## (as 'lib' is unspecified)
### ─────────────────────────────────────────────
### Individual Item Analysis: Bioinspiration (Reversed) & Ecological Scales
### ─────────────────────────────────────────────
# 1. Convert cleaned + reverse-coded Bioinspiration data to long format
# Make sure the columns exist and are not dropped by mistake
bio_long <- bio_items_clean %>%
pivot_longer(
cols = starts_with("Bioinspiration"),
names_to = "item",
values_to = "score"
) %>%
left_join(frame_info, by = "ID") %>%
select(ID, item, score, framingCondition) # <-- This is "immediately after your join"
# Check that items are back
glimpse(bio_long)
## Rows: 7,188
## Columns: 4
## $ ID <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2,…
## $ item <chr> "Bioinspiration-PN2r", "Bioinspiration-PN1", "Bioinsp…
## $ score <dbl> 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 4, 2, 4, 4,…
## $ framingCondition <chr> "sustainable", "sustainable", "sustainable", "sustain…
bio_long %>% dplyr::count(.data$item, .data$framingCondition)
## # A tibble: 36 × 3
## item framingCondition n
## <chr> <chr> <int>
## 1 Bioinspiration-IPI1 bioinspired 201
## 2 Bioinspiration-IPI1 neutral 199
## 3 Bioinspiration-IPI1 sustainable 199
## 4 Bioinspiration-IPI2r bioinspired 201
## 5 Bioinspiration-IPI2r neutral 199
## 6 Bioinspiration-IPI2r sustainable 199
## 7 Bioinspiration-IPI3 bioinspired 201
## 8 Bioinspiration-IPI3 neutral 199
## 9 Bioinspiration-IPI3 sustainable 199
## 10 Bioinspiration-IPI4 bioinspired 201
## # ℹ 26 more rows
# 4. Summarise by individual Bioinspiration item & framing condition
# bio_item_summary <- bio_long %>%
#group_by(item, framingCondition) %>%
#summarise(
#mean = mean(score, na.rm = TRUE),
#sd = sd(score, na.rm = TRUE),
#n = sum(!is.na(score)),
#se = sd / sqrt(n),
#.groups = "drop"
#)
#glimpse(bio_item_summary)
# Convert Ecological Dimension data (cleaned) to long format
eco_joined <- eco %>%
pivot_longer(
cols = starts_with("EcologicalDimension"),
names_to = "item",
values_to = "score"
) %>%
left_join(frame_info, by = "ID")
eco_long <- eco_joined %>%
select(ID, item, score, framingCondition = framingCondition.y)
# Summarise by individual Ecological item & framing condition
eco_item_summary <- eco_long %>%
group_by(item, framingCondition) %>%
summarise(
mean = mean(score, na.rm = TRUE),
sd = sd(score, na.rm = TRUE),
n = sum(!is.na(score)),
se = sd / sqrt(n),
.groups = "drop"
)
# 3a. Plot Bioinspiration Scale items by framing
# First, reorder the items in a separate step
#bio_item_summary <- bio_item_summary %>%
#mutate(item = fct_reorder(item, mean))
# Then plot
#ggplot(bio_item_summary, aes(x = item, y = mean, fill = framingCondition)) +
#geom_col(position = position_dodge(width = 0.8)) +
#geom_errorbar(aes(ymin = mean - se, ymax = mean + se),
#width = 0.2, position = position_dodge(width = 0.8)) +
#coord_flip() +
#labs(
#title = "Bioinspiration Items by Framing Condition",
#x = "Item",
#y = "Mean Score (1–5)"
#) +
#theme_minimal() +
#theme(legend.position = "top")
# Create clean frame_info first (1 row per participant)
frame_info <- dat %>%
filter(sender == "InformedConsent") %>%
select(ID, framingCondition) %>%
distinct()
# Now join to eco_long
eco_long <- eco %>%
pivot_longer(
cols = starts_with("EcologicalDimension"),
names_to = "item",
values_to = "score"
) %>%
left_join(frame_info, by = "ID")
# Remove any existing 'framingCondition.x' and rename 'framingCondition.y'
eco_long <- eco_long %>%
select(-framingCondition.x) %>%
dplyr::rename(framingCondition = framingCondition.y)
# summarise eco items
eco_item_summary <- eco_long %>%
group_by(item, framingCondition) %>%
dplyr::summarise(
mean = mean(score, na.rm = TRUE),
sd = sd(score, na.rm = TRUE),
n = sum(!is.na(score)),
se = sd / sqrt(n),
.groups = "drop"
)
#visualise eco items
ggplot(eco_item_summary, aes(x = reorder(item, -mean), y = mean, fill = framingCondition)) +
geom_col(position = position_dodge(width = 0.8)) +
geom_errorbar(aes(ymin = mean - se, ymax = mean + se),
width = 0.2, position = position_dodge(width = 0.8)) +
coord_flip() +
labs(
title = "Ecological Scale Items by Framing Condition",
x = "Item", y = "Mean Score (1–7)"
) +
theme_minimal() +
theme(legend.position = "top")
We want to find out which items performed well and which we can throw out.
### ─────────────────────────────────────────────
### Bioinspiration Scale – Test for Internal Consistency
### ─────────────────────────────────────────────
# Load necessary libraries
library(dplyr)
library(psych)
# 1. Extract and clean Bioinspiration item responses
#bio_items_clean <- dat %>%
#filter(sender == "Bioinspiration Scale") %>%
#select(starts_with("Bioinspiration")) %>%
#mutate(across(everything(), ~ as.numeric(trimws(as.character(.))))) # Ensure numeric
# 2. Optional: unload plyr if loaded, to avoid conflicts
if ("package:plyr" %in% search()) {
detach("package:plyr", unload = TRUE, character.only = TRUE)
}
## Warning: .onUnload failed in unloadNamespace() for 'plyr', details:
## call: normalizePath(libpath, "/", TRUE)
## error: path[1]="/Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library/plyr": No such file or directory
# 3. Run Cronbach's alpha
bio_alpha <- alpha(bio_items_clean, check.keys = TRUE) # Enables automatic detection of miskeyed items
## Warning in alpha(bio_items_clean, check.keys = TRUE): Some items were negatively correlated with the first principal component and were automatically reversed.
## This is indicated by a negative sign for the variable name.
# 4. Print results
cat("Cronbach's Alpha (raw):", round(bio_alpha$total$raw_alpha, 3), "\n\n")
## Cronbach's Alpha (raw): 0.019
print(bio_alpha$item.stats)
## n raw.r std.r r.cor r.drop
## ID 599 0.998932354 0.2809661 0.1817480 0.170450028
## Bioinspiration-PN2r- 599 0.001723751 0.2722943 0.1751743 -0.004191192
## Bioinspiration-PN1 599 0.184786526 0.7820897 0.7725811 0.179609099
## Bioinspiration-IPI4 599 0.200656779 0.8227335 0.8283721 0.195528897
## Bioinspiration-VRtN4r 599 0.226975186 0.7052809 0.6723654 0.221220138
## Bioinspiration-VRtN2 599 0.216031059 0.8444132 0.8469174 0.210296138
## Bioinspiration-IPI1 599 0.205947890 0.8144334 0.8190196 0.200714965
## Bioinspiration-VRtN1 599 0.212816050 0.7928477 0.7831156 0.207384911
## Bioinspiration-IPI3 599 0.134599507 0.8012449 0.7999483 0.129520950
## Bioinspiration-VRtN3 599 0.177003864 0.7879547 0.7734617 0.171782659
## Bioinspiration-PN3 599 0.086154330 0.5881232 0.5459042 0.081126579
## Bioinspiration-IPI2r 599 0.076438718 0.4845056 0.4186970 0.070639937
## Bioinspiration-PN4 599 0.087639090 0.6685188 0.6349002 0.081817562
## mean sd
## ID 312.335559 180.7781561
## Bioinspiration-PN2r- 624.911519 1.0790731
## Bioinspiration-PN1 3.550918 0.9764767
## Bioinspiration-IPI4 3.781302 0.9731776
## Bioinspiration-VRtN4r 3.268781 1.1046619
## Bioinspiration-VRtN2 3.477462 1.0953274
## Bioinspiration-IPI1 3.781302 0.9952652
## Bioinspiration-VRtN1 3.442404 1.0359451
## Bioinspiration-IPI3 3.933222 0.9426080
## Bioinspiration-VRtN3 3.455760 0.9819250
## Bioinspiration-PN3 3.554257 0.9234828
## Bioinspiration-IPI2r 3.404007 1.0634002
## Bioinspiration-PN4 3.158598 1.0694396