```{r importing data and defining functions}
surveydata = read.csv("data/surveydata.csv")
files = list.files(path ="data", pattern="^10.*.csv$", full.names = TRUE)
expdata_full = do.call(bind_rows,
lapply(files, function(x) read.csv(x, stringsAsFactors = FALSE)))
expdata <- expdata_full %>%
select(participant,
words,
mask,
corr_ans,
category,
nonwords,
tweets,
topic,
task,
keypress,
RT,
corr,
participant,
AMP_nonword_rating.response,
AMP_word_rating.response,
AMP_random_rating.response,
LDT_rating.response,
AMP_loop.thisN,
AMP_trials.thisN)
# This function will check to see whether an observation is
# an outlier based on median absolute deviation (MAD)
out_mad <- function(x, thres = 3, na.rm = TRUE) {
abs(x - median(x, na.rm = na.rm)) >= thres * mad(x, na.rm = na.rm)
}
out_replace = function(dataframe, cols, rows, newValue = NA) {
if (any(rows)) {
set(dataframe, rows, cols, newValue)
}
}
Recoding and Data Checks
Let's do some recoding and variable manipulation.
For some reason PsychoPy records correct as 0 and incorrect as 1, so let's switch those.
We'll add a new variable caled "wordcat" that tells us whether the word is moral, nonmoral, or a nonword.
We'll split up the category variable into "foundation" and "valence" in case we are interested in the two things separately at any point.
Looks like I accidentally named the AMP different things in condition 1 and condition 2. To remedy this, we'll coalesce the 'AMP_loop.thisNandAMP_trials.thisN` variables together into one column.
First, let's make sure that we have all of the data and that we don't have duplicate subject numbers. For this we need to get the LDT trials and the AMP trials specifically, and then check to see how many trials we have per subject.
Now we need to do a little bit of cleaning to try our best to get rid of outlying STRTs as well as those who didn't actually try in the task(s). I'm going to look at a few things: a) who they self-reported random responses, b) chance accuracy in the LDT, c) distributions of response times, d) the participants that the RA's called out as not paying attention
First, lets look at self-reported ratings. These are going to be fairly interesting.
Looks like people were more willing to admit they responded randomly in the LDT (1001,1013,1023,1029,1032,1063) than the AMP (1032, 1038, 1080). Let's look at button presses in the AMP and the LDT.
```{r press proportions}
df1 <- expdata_w %>% filter(task == "LDT" | task == "AMP") %>%
group_by(participant, task, keypress) %>%
summarize(n = n())
df1 <- df1 %>% spread(keypress, n) %>%
summarize(ratio = left/right)
median(df1$ratio) + 3 * sd(df1$ratio)
There are a few clear outliers here. 1031 and 1074. 1074 was one of the ones called out by the RA's as not trying at all. Would probably be worth filtering if they pop up again.
Doesn't look like it. Let's look at this based on words. Should be interesting but I don't know if we have enough data for strong conclusions here yet.
```{r}
LDT_trials <- expdata_w %>%
group_by(words) %>%
filter(task == "LDT") %>%
summarize(corr = mean(corr, na.rm=TRUE))
df <- LDT_trials %>% group_by(words) %>% summarize(corr = mean(corr), n = n())
df_wordouts <- df %>%
select(words, corr, n) %>%
mutate_if(is.numeric, funs(mad = out_mad))
table(df_wordouts$corr_mad)
Only two words are outliers, the non-word "ammessment," and the non-word "tove" Only 28% of people got ammessment and only 27% got tove correct.
Moral words were more accurately responded to as words than were nonwords. Looks like there are a few people that hover around chance in their responses but they aren't technically outliers.
Now let's looks at reaction times in the AMP and the LDT. I'm not sure that we are going to be able to pick out people who are just clicking through based on reaction time, but I'll do some cross referencing with the people who the RA's said were just clicking through without looking at the screen and see if I can get an idea.
Pretty highly right-skewed. Let's remove outliers within subjects and conditions and then check to see how we look.
```{r}
df_indouts <- df %>%
group_by(participant, task) %>%
select(participant,
RT,
foundation,
valence,
wordcat,
task,
words,
corr,
trialnum,
nonwords,
keypress) %>%
mutate_if(is.numeric, funs(mad = out_mad))
# Filtering based on that column
expdata_rtfilt <- filter(df_indouts, RT_mad == FALSE)
# Additionally replacing RTs less than 100msec with 100msec, following Whelan (2008)
expdata_rtfilt <- expdata_rtfilt %>%
mutate(RT = ifelse(RT <= .1, .1, RT))
ggplot(expdata_rtfilt, aes(x = RT)) + geom_histogram()
Looks a little better. Let's see if we have any partipant outliers. Here I'll go back to the unfiltered dataset and run the same procedure on the participant means.
Looks like we have a few outliers, but I'm hestitant to toss them since they're on the long end, suggesting that they are thinking about their answers rather than just thumbing through mindlessly. We can do some thinking on this.
So the only cleaning that that we've done so far is re: reaction times. We can perhaps do some more thoughtful cleaning in the future. Now that we have filtered, we can go on to some some preliminary analysis. Let's do some EDA plotting
LDT EDA
# First, let's look at neutral words vs. nonwords vs. moral words
plt_data <- expdata_rtfilt %>%
filter(task == "LDT" & wordcat != "nonword") %>%
group_by(participant, wordcat) %>%
mutate(meanrt = mean(RT)) %>%
group_by(participant, wordcat) %>%
summarize(wordrt = mean(RT),
corr = mean(corr),
n = n()) %>%
ungroup()
plt_data %>% do(broom::tidy(t.test(corr~wordcat, data=.)))
# Box plot of correctness by category
plt1 <- ggplot(plt_data, aes(x = wordcat, y = corr, fill = wordcat)) +
geom_boxplot() + coord_flip() + labs(title = "Response Correctness by Category", x = "Word Category", y = "Mean Correctness") + theme_minimal() + theme(legend.title=element_blank())
# Density plot of correctness by category
plt2 <- ggplot(plt_data, aes(x = corr, fill = wordcat, color = wordcat)) +
geom_density(alpha = .8) + labs(title = "Response Correctness by Category", x = "Mean Correctness") + theme_minimal() + theme(legend.title=element_blank())
# Boxplot of RT by category
plt3 <- ggplot(plt_data, aes(x = wordcat, y = meanrt, fill = wordcat)) +
geom_boxplot() + coord_flip() + labs(title = "Reaction Times by Category", x = "Word Category", y = "Mean Reaction Time") + theme_minimal() + theme(legend.title=element_blank())
#Density plot of RT by category
plt4 <- ggplot(plt_data, aes(x = meanrt, fill = wordcat, color = wordcat)) +
geom_density(alpha = .8) + labs(title = "Reaction Times by Category", x = "Mean Reaction Time") + theme_minimal() + theme(legend.title=element_blank())
grid.arrange(plt1, plt2, plt3, plt4)
# Importing the weights to see how they correlate with response times and accuracy.
weights = read.csv("data/mfd_weights.csv")
weights <- weights %>%
select(-X) %>%
rename(words = word)
LDT_words <- expdata_rtfilt %>%
filter(task == "LDT" & wordcat == "moralword") %>%
group_by(words) %>%
summarize(meancorr = mean(corr),
meanrt = mean(RT),
cat = paste(unique(foundation), collapse = ', '),
val = paste(unique(valence), collapse = ', '))
LDT_weighted <- left_join(LDT_words, weights, by = "words")
plot_data <- filter(LDT_weighted, !is.na(weight))
ggplot(plot_data, aes(x = weight, y = meancorr)) +
geom_point() +
geom_smooth(method='lm',formula=y~x, se=F)
ggplot(plot_data, aes(x = weight, y = meanrt)) +
geom_point() +
geom_smooth(method='lm',formula=y~x, se=F)
# Looks like nothing really going on here.
AMP EDA
Before we do anything with the AMP, we need to manipulate the data in a way that it is able to be analyzed (turning it from wide to long).
Okay now we can do some investigating. I'm going to look at the following:
- Plotting differences in responses between moral primes and nonword primes
- Whether the valence of primes leads to differences in responses (valence or speed)
- Whether the weighting of the word in the dictionary is associated with differences in response times
There's a discussion to be had about whether these plots should be done at the word level (each word is an observation within each category) or at the participant level (each participant is one observation within each category). Each gives us different views of the data.
I still haven't incorporated the survey data but I'll do that in the next section.
```{r}
plot_data <- AMP_trials_long %>%
group_by(participant) %>%
mutate(meanRT = mean(RT)) %>%
ungroup() %>%
#filter(!(prime_foundation %in% c("control", "nonword"))) %>%
group_by(prime_val) %>%
filter(prime_target == "prime") %>%
summarize(mean_val_response = mean(val_response), rt = mean(RT), meanRT = mean(meanRT)) %>%
mutate(phi = (1/(rt/meanRT)) * mean_val_response) %>%
ungroup()
plot_data %>% do(broom::tidy(t.test(mean_val_response~prime_val, data=.)))
p1 <- ggplot(plot_data, aes(x = mean_val_response, fill = prime_val, color = prime_val)) +
geom_density(alpha = .8) +
labs(title = "Mean valence of responses by prime category",
x = "Valence",
y = "Density")
p2 <- ggplot(plot_data, aes(x = rt, fill = prime_val, color = prime_val)) +
geom_density(alpha = .8) +
labs(title = "Mean RT of responses by prime category",
x = "RT",
y = "Density")
p3 <- ggplot(plot_data, aes(x = phi, fill = prime_val, color = prime_val)) +
geom_density(alpha = .8) +
labs(title = "Salience of responses by prime category (phi)",
x = "Salience",
y = "Density")
grid.arrange(p1, p2, p3, ncol = 2)
Interesting! Looks like the procedure works, but need to test statistically to be sure. Vice words elicit negative valence, virtue words elicit positive valence, and nonwords are somewhere in the middle. Nothing really interesting for RTs. Let's look at the foundations and the weights.
plot_data <- AMP_trials_long %>% group_by(words, prime_foundation, prime_val) %>%
filter(prime_target == "prime") %>%
summarize(mean_val_response = mean(val_response), meanrt = mean(RT), n = n())
ggplot(plot_data, aes(x = mean_val_response,
y = prime_foundation,
fill = prime_val,
color = prime_val)) +
geom_density_ridges(alpha = .8,
scale = .9,
jittered_points = TRUE,
position = "raincloud") +
scale_fill_manual(values=c("#FF6C67", "#00B2FD", "#00BE0D")) +
scale_color_manual(values=c("#FF6C67", "#00B2FD", "#00BE0D")) +
labs(title = "Mean salience of responses by foundation",
x = "Salience",
y = "Density")
Very neat. Looks like our valences line out quite nicely with the words in the moral categories. Note that the controls aren't plotting because there is only one observation per category (the word "pleasant" and the word "unpleasant"). The mean for "pleasant is $.201$ and the mean for "unpleasant" is $-.34$ so they align roughly as expected. Some of the nonword primes have low numbers of observations (n ~= 4), but as we recruit more participants this should even out.
```{r AMP weights}
AMP_weights <- left_join(AMP_trials_long, weights, by = "words")
Another interesting finding. The dictionary weights for virtues and vices are correlated with people's reported valence. Both are significant. Will be interesting to see whether this replicates in a larger sample and with our possibly refined weighting scheme. I think that's good for now with this data. I'll mess with the survey data to see whether it's interesting or not.
# Survey Data Wrangling
```{r survey data wrangling}
#Dropping unneeded rows and columns
surveydata_w <- surveydata %>% slice(-c(0,1)) %>% select(-c(1:17))
# Other misc renaming
surveydata_w <- surveydata_w %>%
rename("participant" = "Q1",
"age" = "Q1.1",
"sex" = "Q1.2",
"eth" = "Q1.3",
"pol_or" = "Q5",
"religion" = "Q24",
"nat_speak1" = "Q4.1",
"nat_speak2" = "Q4.2",
"nat_speak3" = "Q4.3",
"handed" = "Q23")
# MFQ Renaming
surveydata_w <- surveydata_w %>%
rename("MFQ_care1" = "Q2.1_1",
"MFQ_fair1" = "Q2.1_2",
"MFQ_loyalty1" = "Q2.1_3",
"MFQ_authority1" = "Q2.1_4",
"MFQ_purity1" = "Q2.1_5",
"MFQ_math" = "Q2.1_6",
"MFQ_care2" = "Q2.1_7",
"MFQ_fair2" = "Q2.1_8",
"MFQ_loyalty2" = "Q2.1_9",
"MFQ_authority2" = "Q2.1_10",
"MFQ_purity2" = "Q2.1_11",
"MFQ_care3" = "Q2.1_12",
"MFQ_fair3" = "Q2.1_13",
"MFQ_loyalty3" = "Q2.1_14",
"MFQ_authority3" = "Q2.1_15",
"MFQ_purity3" = "Q2.1_16",
"MFQ_care4" = "Q2.2_1",
"MFQ_fair4" = "Q2.2_2",
"MFQ_loyalty4" = "Q2.2_3",
"MFQ_authority4" = "Q2.2_4",
"MFQ_purity4" = "Q2.2_5",
"MFQ_good" = "Q2.2_6",
"MFQ_care5" = "Q2.2_7",
"MFQ_fair5" = "Q2.2_8",
"MFQ_loyalty5" = "Q2.2_9",
"MFQ_authority5" = "Q2.2_10",
"MFQ_purity5" = "Q2.2_11",
"MFQ_care6" = "Q2.2_12",
"MFQ_fair6" = "Q2.2_13",
"MFQ_loyalty6" = "Q2.2_14",
"MFQ_authority6" = "Q2.2_15",
"MFQ_purity6" = "Q2.2_16")
surveydata_w <- surveydata_w %>%
mutate_at(vars(contains("MFQ")), funs(as.numeric)) %>%
mutate(care_mean = rowMeans(select(., starts_with("MFQ_care")), na.rm=TRUE),
fair_mean = rowMeans(select(., starts_with("MFQ_fair")), na.rm=TRUE),
loyal_mean = rowMeans(select(., starts_with("MFQ_loyalty")), na.rm=TRUE),
auth_mean = rowMeans(select(., starts_with("MFQ_authority")), na.rm=TRUE),
sanc_mean = rowMeans(select(., starts_with("MFQ_purity")), na.rm=TRUE),
MFQ_var = matrixStats::rowVars(as.matrix(select(., starts_with("MFQ"))), na.rm=TRUE))
surveydata_w <- surveydata_w %>%
mutate(MFQ_mean = rowMeans(select(., care_mean, fair_mean, loyal_mean, auth_mean, sanc_mean), na.rm=TRUE))
Looks like we have a few participants that did not actually fill out the MFQ (1003, 1020, 1021, 1035, 1040, 1044, 1045, 1060). Let's drop them from further analysis.
Looks like people might respond slightly faster to moral words if they have high moral salience but the effect is small if it's anything. Let's look at within-domain salience.
Let's look at whether native speakers respond faster than non-native speakers.
Seems clear that native speakers outperform non-native speakers in the AMP. Might be worth controlling for in a future analysis. Let's move on to effects of within-domain salience.
plot_data <- fulldata %>%
group_by(participant, words) %>%
filter(task == "LDT") %>%
summarize(meanrt = mean(RT),
meancorr = mean(corr),
care_sal = mean(care_mean),
fair_sal = mean(fair_mean),
loyal_sal = mean(loyal_mean),
auth_sal = mean(auth_mean),
sanc_sal = mean(sanc_mean),
foundation = paste(unique(foundation), collapse = ', '),
valence = paste(unique(valence), collapse = ', '),
n = n())
p1 = ggplot(data = filter(plot_data, foundation == "care"),
aes(x = care_sal, y = meanrt, color = valence)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ x)
p2 = ggplot(data = filter(plot_data, foundation == "fairness"),
aes(x = fair_sal, y = meanrt, color = valence)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ x)
p3 = ggplot(data = filter(plot_data, foundation == "loyalty"),
aes(x = loyal_sal, y = meanrt, color = valence)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ x)
p4 = ggplot(data = filter(plot_data, foundation == "authority"),
aes(x = auth_sal, y = meanrt, color = valence)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ x)
p5 = ggplot(data = filter(plot_data, foundation == "sanctity"),
aes(x = sanc_sal, y = meanrt, color = valence)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ x)
grid.arrange(p1, p2, p3, p4, p5, ncol = 2)
Seems fairly interesting. Some more thinking to do though. Let's turn to the AMP for now.
Interesting. We get a slightly positive correlation between MFQ salience and valence, but it's not significant. Not exactly what I expected. Let's look at individual domains.
Pretty uninterpretable IMHO. Main effect of valence, but not really anything with MFQ salience as far as I can tell. Let's look at a couple of the other self-report things., namely religiosity and political affiliation.
plot_data <- AMP_trials_long %>%
filter(prime_cat == "moralword") %>%
group_by(participant) %>%
filter(prime_target == "prime") %>%
mutate(meanrt = psych::harmonic.mean(RT, na.rm=TRUE)) %>%
ungroup() %>%
group_by(participant, prime_foundation) %>%
summarize(rt = psych::harmonic.mean(RT),
meanval = mean(val_response),
phi = mean((1/(rt/meanrt)) * meanval),
pol_or = mean(as.numeric(as.character(pol_or)), na.rm=TRUE)) %>%
ungroup()
ggplot(plot_data, aes(x = pol_or, y = phi)) + geom_point() + geom_smooth(method= "lm", formula=y~x) + facet_wrap(.~prime_foundation) + labs(title = "Relationship between political orientation and moral salience within foundations", x = "Political Orientation", y = "Salience (φ)") + theme_minimal()
Let's follow up on that political orientation plot and just do some bar plots.