Below we describe the analysis done for the paper An Empirical Study of Bots in Software Development: Characteristics and Challenges from a Practitioner’s Perspective. Note that this R Markdown file can be turned into an R script to be modified in case you wish to analyse something different in our data.
We divide our analysis into several parts, and details regarding motivation and findings for each part are found in the manuscript. Here, we focus on presenting (i) the goals and (ii) steps for each analysis as reproducible scripts.
This will clean your environment to avoid loading unnecessary functions or variables.
rm(list = ls())
We begin by installing and loading the required packages.
# You can install packages in case they are not installed already.
# install.packages(c("tidyverse", "likert", "gridExtra")
library(tidyverse)
library(likert)
library(gridExtra)
Next, we load the results from the survey into a data frame.
survey_answers <- read.csv2("complete_survey_responses.csv", header = T)
# We also create a graphical object that has the default theme for ggplot throughout this script.
PLOT_THEME <- theme_minimal(base_size = 14) +
theme(plot.title = element_text(size = 14, face = "bold", hjust = 0.5),
panel.grid = element_blank(), axis.line = element_line(size = 1), axis.ticks = element_line(size = 1),
strip.background = element_rect(colour = "white", fill = "aliceblue"), legend.position = "bottom")
Goal: Get an overview of the participants’ answers about bot definitions.
We begin by analysing the questions regarding bot definition. Our goal is to see: (i) which tool descriptions practitioners perceive as bots, and (ii) map our participants to the defined personas.
The first step is to select the corresponding questions (columns) from the survey data. Since the survey data is quite extensive, this will make data wrangling easier. We also clean the column names to use short IDs and have cleaner plots. This helps when printing the data. The original text for questions can be found in the CSV file and also in Table 2 of the paper.
DEFINITION_QUESTIONS_COL <- 10:41
#Selects columns and turns values into factors to be used as likert scales.
bot_definitions_df <- select(survey_answers,DEFINITION_QUESTIONS_COL) %>% mutate_all(factor)
## Note: Using an external vector in selections is ambiguous.
## ℹ Use `all_of(DEFINITION_QUESTIONS_COL)` instead of `DEFINITION_QUESTIONS_COL` to silence this message.
## ℹ See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
## This message is displayed once per session.
colnames(bot_definitions_df) <- paste("Q",1:ncol(bot_definitions_df), sep="") #Replaces questions text for IDs
print(paste("Total number of 'bot definition' questions:",ncol(bot_definitions_df),
"; Total number of participants:",nrow(bot_definitions_df)))
## [1] "Total number of 'bot definition' questions: 32 ; Total number of participants: 111"
We create a diverging plot to get an overview of how participants perceive the presented tools as bots. Note that the answers correspond to the following scale:
likert_bot_definition <- likert(bot_definitions_df)
plot(likert_bot_definition) + ggtitle("Is the following tool a bot?") + PLOT_THEME
Goal: Calculate persona scores for each participant.
We manually mapped each question to a persona into a CSV file used for this part of our analysis.
persona_map_df <- read.csv2("persona_mapping.csv", header = T)
persona_map_df
As described in the paper, we assign a persona score between [-2,2] to each participant by (i) rewarding them whenever they give a high likert score to a question of a corresponding persona, and (ii) punishing them for giving high scores for questions of another persona. For instance, a participant that gave high scores for Sam (smartness) questions, but low scores for Alex (autonomy) will have a higher Sam score than another participant that gave high score for Sam but also high score for Alex.
We begin by extracting the questions for each corresponding personas, and the questions that will punish each persona score (i.e., questions of opposing personas).
#Questions that increase each participant's persona scores
qid_alex_rewards <- persona_map_df %>% filter(Alex == "x") %>% pull(QuestionID) %>% as.character
qid_charlie_rewards <- persona_map_df %>% filter(Charlie == "x") %>% pull(QuestionID) %>% as.character
qid_sam_rewards <- persona_map_df %>% filter(Sam == "x") %>% pull(QuestionID) %>% as.character
#Questions that reduce from each participant's persona scores
qid_alex_punish <- persona_map_df %>% filter(Alex == "") %>% pull(QuestionID) %>% as.character
qid_charlie_punish<- persona_map_df %>% filter(Charlie == "") %>% pull(QuestionID) %>% as.character
qid_sam_punish <- persona_map_df %>% filter(Sam == "") %>% pull(QuestionID) %>% as.character
Finally, using the responses from all participants, we create individual data frames for each persona’s reward. We do that by converting the likert scales to the corresponding reward score (details in the paper). For each participant we sum their reward scores for each persona.
# Function that convert the likert scores from [1,5] to [-2,2]
# Recall that before we converted the numerical values into factors.
convert_to_reward <- function(x) {case_when(
x ==1 ~ -2,
x ==2 ~ -1,
x ==3 ~ 0,
x ==4 ~ 1,
x ==5 ~ 2)}
#Data frames for the reward scores. Adds a Sum column with all scores
alex_reward_df <- bot_definitions_df %>% select(!!qid_alex_rewards) %>%
mutate_all(.funs = convert_to_reward) %>% mutate(Sum = rowSums(., na.rm=T))
charlie_reward_df <- bot_definitions_df %>% select(!!qid_charlie_rewards) %>%
mutate_all(.funs = convert_to_reward) %>% mutate(Sum = rowSums(., na.rm=T))
sam_reward_df <- bot_definitions_df %>% select(!!qid_sam_rewards) %>%
mutate_all(.funs = convert_to_reward) %>% mutate(Sum = rowSums(., na.rm=T))
We do the same for the remaining data frames with punishing scores.
# Function that convert the likert scores from [1,5] to [2,-2]
convert_to_punish <- function(x) {case_when(
x ==1 ~ 2,
x ==2 ~ 1,
x ==3 ~ 0,
x ==4 ~ -1,
x ==5 ~ -2)}
#Data frames for the punishing scores.
alex_punish_df <- bot_definitions_df %>% select(!!qid_alex_punish) %>%
mutate_all(.funs = convert_to_punish) %>% mutate(Sum = rowSums(., na.rm=T))
charlie_punish_df<- bot_definitions_df %>% select(!!qid_charlie_punish) %>%
mutate_all(.funs = convert_to_punish) %>% mutate(Sum = rowSums(., na.rm=T))
sam_punish_df <- bot_definitions_df %>% select(!!qid_sam_punish) %>%
mutate_all(.funs = convert_to_punish) %>% mutate(Sum = rowSums(., na.rm=T))
Lastly, we aggregate the neutral scores, i.e., participants that answered “Unclear” or that skipped defining one of the tools. This will be used later to obtain the participant’s average score in each persona.
count_neutral_values <- function(row_values){
#Participant either "Skiped" (NA), or "Unclear" (score == 3)
neutral_values <- which(is.na(row_values) | row_values == 3)
return(length(neutral_values))
}
aggregate_neutral_scores <- function(persona_df) {
modified_df <- persona_df %>%
rowwise() %>% do(row_values = as_tibble(.)) %>%
mutate(NeutralValues = count_neutral_values(row_values)) %>% unnest(cols = c(row_values))
return(modified_df)
}
neutral_scores <- aggregate_neutral_scores(bot_definitions_df) %>% select(NeutralValues)
Using all three information (reward, punishing and neutral scores), we calculate the corresponding persona association score for each participant. This final score is an average obtained by summing the reward and punishing scores and then dividing it by the number of valid scores given by the participant.
Ultimately, each participant is scored to each persona as a value between -2 (i.e., unlikely to belong to that persona) to 2 (i.e., likely to belong to that persona). We will use these values throughout the rest of our analysis to characterize our sample and analyse correlations between participant’s answers. The resulting scores_df
dataframe will be the baseline for the remained of our analysis.
num_of_questions <- ncol(bot_definitions_df)
charlie_scores<- (charlie_reward_df$Sum + charlie_punish_df$Sum) / (num_of_questions - neutral_scores$NeutralValues)
sam_scores <- (sam_reward_df$Sum + sam_punish_df$Sum) / (num_of_questions - neutral_scores$NeutralValues)
alex_scores <- (alex_reward_df$Sum + alex_punish_df$Sum) / (num_of_questions - neutral_scores$NeutralValues)
scores_df <- data.frame(Charlie = charlie_scores, Alex = alex_scores, Sam = sam_scores)
scores_df
Goal: Identify whether a specific persona is more prominent among our survey respondants.
The result of the previous steps (scores_df
) contains the persona association score for each participant. In order to control for the participants’ varied experience in using bots, we join this data frame with the original survey data to identify which participants actually used bots. This will be stored in a column named BotUsers
.
# Constant values used for plotting and filtering of data.
BOT_USAGE_COLUMN <- 43
BOT_EXPERIENCE_LABEL<- "Bot Experience"
NON_BOT_USER_LABEL <- "No Bots Used"
#Copy the scores_df into a new datafram to keep this part of the analysis self-contained
personas_df <- scores_df
personas_df <- cbind(BotUsers = survey_answers[,BOT_USAGE_COLUMN], scores_df);
personas_df <- personas_df %>% mutate_at(.vars = c("BotUsers"), .funs = function(x){if_else(x == 1,BOT_EXPERIENCE_LABEL,NON_BOT_USER_LABEL)})
We now evaluate the prevalence of each persona in the data. Therefore, we initially only evaluate which score is highest for each participant. If all scores are below a threshold
(in our case, threshold = 0
) we count the participant as belonging to no specific persona (“None”).
# Function to classify a participant (row) based on its scores.
identify_main_persona <- function(participant){
# Set threshold for participants belonging to no persona
threshold <- 0
hasPersona <- participant$Charlie > threshold | participant$Alex > threshold | participant$Charlie > threshold
main_persona <- if_else(hasPersona, colnames(participant)[max.col(participant)], "None")
return(main_persona)
}
main_personas_df <- personas_df %>% select(-BotUsers) %>% #Temporarily drop the BotUsers column
rowwise() %>% do(participant = as_tibble(.)) %>%
mutate(MainPersona=identify_main_persona(participant)) %>% unnest(cols = c(participant))
#Bring back the BotUsers column and count occurencies of each main persona
main_personas_df <- cbind(BotUsers = personas_df$BotUsers, main_personas_df)
personas_frequency <- main_personas_df %>% group_by(BotUsers,MainPersona) %>% tally(name = "Frequency")
personas_frequency
Now we plot the results into simple bar charts to verify if we have a prominent persona among our participants.
Note that the plot below reveals that participants that have used bots fit more under the Charlie persona, where chatting or communicating with a bot is a relevant aspect to define a bot. In constrast, participants that have not used a bot before match the Sam persona where the smartness of a bot is one of its main defining charactersitics. We further discuss the implications of those results in connection to our findings in the paper.
max_frequency <- max(personas_frequency$Frequency) #Saves highest value to set plot's scales' limits.
persona_palette <- c("None" = "#AED9D6","Sam" = "#5BB4AC", "Charlie" = "#985f99", "Alex" = "#D9B466")
ggplot(personas_frequency, aes(x= MainPersona, y=Frequency, fill=MainPersona))+
geom_bar(width = 1, stat = "identity", colour = "black") + coord_flip() +
scale_y_continuous(limits = c(0, max_frequency), breaks = seq(0,max_frequency, by=2)) +
PLOT_THEME + scale_fill_manual(values = persona_palette) +
theme(legend.position = "None") +
facet_grid(personas_frequency$BotUsers ~ .)
Goal: Get an overview of how participants’ rate different reasons to use a bot in software development.
We also asked participants (that have used a bot in software development) to rate the reasons for using bots. The list of reasons used on our survey were obtained from our interview data. Participants rated each reason according to the following likert scale:
Below, we extract that data from our survey data and create diverging plots to see which are the main reasons that motivate participants to use Bots in software development.
# Gets columns where participants scored the reasons and removes participants that never used a bot.
BOT_REASONS_COL <- c(45:54)
bot_reasons_df <- survey_answers %>% select(BOT_REASONS_COL) %>%
filter_all(any_vars(!is.na(.))) %>% mutate_all(factor)
## Note: Using an external vector in selections is ambiguous.
## ℹ Use `all_of(BOT_REASONS_COL)` instead of `BOT_REASONS_COL` to silence this message.
## ℹ See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
## This message is displayed once per session.
# Similar to the questions text, we summarize the reasons to better plot the data.
# The complete text for each reason can be found in the CSV data.
short_descriptions <- c("Reduce Boring Tasks", "Information Integration","Health Monitoring",
"Integration Quality","Common Information Gateway", "ChatOps",
"Automatic Resource Cleanup", "Analyse Big Data","Problem Notifications",
"Maintain Dependencies")
colnames(bot_reasons_df) <- short_descriptions
likert_reasons <- likert(bot_reasons_df)
plot(likert_reasons) + ggtitle("Reasons to Use a Bot") + PLOT_THEME