Install Packages

if (!require("pacman")) install.packages("pacman", repos = "https://cloud.r-project.org")
pacman::p_load(
  tidyverse,
  jsonlite,
  ggpubr,
  psych
)

Load Libraries

library(tidyverse)  
library(jsonlite)
library(ggpubr)
library(pacman)
library(psych)

Load & Prep Data

# 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)

Score Calculations

### ─────────────────────────────────────────────
### 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

Create table to compare framing conditions

### ─────────────────────────────────────────────
### 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")

Summary & Plots

#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

Interpretation of summary stats:

# 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()

Some more plots…

# 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()

Test For Spillover Effects

i.e., Did exposure to the bioinspired vignettes results in pps. rating the tech as more sustainable? and vice versa.

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()

Interpretation:


Everything below this point is broken

Performance of Individual Scale Items

Bioinspiration Scale

# 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"
  )

Plot the Bioinspiration Scores by Individual Item

# 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")

Ecological Dimension Scale

# 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"
  )

Plot the Ecological Scale Items

#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")


Scale Purification for Bioinspiration Items

We want to find out which items performed well and which we can throw out.

Run Cronbach’s Alpha

### ─────────────────────────────────────────────
### 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