TITLE-ABS-KEY ( learning AND analytics AND education ) AND PUBYEAR > 2010 AND PUBYEAR < 2023 AND ( LIMIT-TO ( LANGUAGE , "English" ) ) AND ( LIMIT-TO ( SUBJAREA , "COMP" ) OR LIMIT-TO ( SUBJAREA , "SOCI" ) OR LIMIT-TO ( SUBJAREA , "PSYC" ) ) AND ( LIMIT-TO ( SRCTYPE , "j" ) ) AND ( LIMIT-TO ( DOCTYPE , "ar" ) ) AND ( LIMIT-TO ( AFFILCOUNTRY , "United States" ) )
library(igraph)
library(splitstackshape)
# load in dataset from scopus
scopus <- read.csv("scopus.csv")
# only keep the authors column (column 1)
authors_df <- as.data.frame(scopus[, 1])
# rename the column to AU
colnames(authors_df) <- "AU"
# decompose cells of authors' names into columns to create an adjacency list
authors <- cSplit(authors_df, splitCols = "AU", sep = ";", direction = "wide", drop = TRUE)
# transform the authors dataframe to a matrix
authors_matrix <- as.matrix(authors)
authors_matrix <- tolower(authors_matrix)
# create an empty matrix with two columns
master_authors_edgelist <- matrix(NA, 1, 2)
# for the number of columns in the matrix - 1, create an edgelist and append it to the master edgelist
for (i in 1:(ncol(authors_matrix) - 1)) {
authors_edgelist <- cbind(authors_matrix[, i], c(authors_matrix[, -c(1:i)]))
master_authors_edgelist <- rbind(master_authors_edgelist, authors_edgelist)
master_authors_edgelist <- master_authors_edgelist[!is.na(master_authors_edgelist[, 2]), ]
master_authors_edgelist <- master_authors_edgelist[master_authors_edgelist[, 2] != "", ]
}
# create a graph of the network of authors
authors_graph <- graph.edgelist(master_authors_edgelist, directed = FALSE) # returns: v = 1,263 and e = 3,459
The topic I selected for this assignment is learning analytics in education because I’m interested in the ways data from learners and teachers can be analyzed to examine how teaching and learning occurs. These insights can inform the design and implementation of teaching strategies, learning interventions and tools, etc., which can improve teachers’ instructional practices and enhance students’ learning experiences and outcomes.
I decided to look at the timespan from 2011 to 2022 because learning analytics emerged as a field in 2011. Since the field relatively new, I wanted to gain a better understanding of the researchers who have contributed to its development and how they have worked together.
I chose to focus on journal articles because they undergo a rigorous peer review process before being published, ensuring that the research conducted is high quality. I specifically limited the articles to those published in the United States because I wanted to learn more about the collaborations between/among U.S. researchers.
The dimensions of the initial adjacency list matrix is 421 x 31. This means that there are a total of 421 journal articles in the dataset, and that at least one of these articles has 31 authors.
After generating the edgelist, there are 1,263 unique authors in the network.
In the network, there are 3,459 connections. These connections are undirected, meaning that a connection established between two authors is reciprocated. For example, there aren’t instances where an author (Author 1) is connected to the other author (Author 2), but Author 2 isn’t connected to Author 1.
# only keep rows where there's an author ID
scopus <- scopus[scopus$Author.s..ID != "[No author id available]", ]
# create adjacency list of authors' names
## get authors' names, which are in the 1st column of the dataset
authors_name <- as.data.frame(scopus[, 1])
## rename the column
colnames(authors_name) <- "AU"
## make authors' names lowercase
authors_name$AU <- tolower(authors_name$AU)
## decompose cells of authors' names into columns
authors_name_split <- cSplit(authors_name, splitCols = "AU", sep = ";", direction = "wide", drop = TRUE)
# create adjacency list from authors' IDs
## get authors' IDs, which are in the 3rd column of the dataset
authors_id <- as.data.frame(scopus[, 3])
## rename the column
colnames(authors_id) <- "AU"
## decompose cells of authors' IDs into columns
authors_id_split <- cSplit(authors_id, splitCols = "AU", sep = ";", direction = "wide", drop = TRUE)
# check to see if the dimensions of the name and id adjacency lists match
# dim_authors_name_split; dim(authors_name_split) ## both return 421 x 31
# unlist the name and ID adjacency lists to create a dataframe w/ the authors' names and and their associated ID
authors_name_id_unlisted = data.frame(id = unlist(authors_id_split), names = unlist(authors_name_split))
## only keep rows where there's an author ID
authors_name_id_unlisted <- authors_name_id_unlisted[!is.na(authors_name_id_unlisted$id), ]
## only keep 1 row for each author ID
authors_name_id_unlisted <- authors_name_id_unlisted[!duplicated(authors_name_id_unlisted$id), ]
# dim(authors_name_id_unlisted) ## returns: 1,316 x 2
# get the most prolific authors by ID
## create a copy of "authors_name_id_unlisted"
authors_name_id_pub_count_unlisted <- authors_name_id_unlisted
## unlist the ID adjacency list and get the frequency that each ID appears
id_pub_count <- as.data.frame(table(unlist(authors_id_split)))
## add a column called "pub_count" to "authors_name_id_pub_count_unlisted" w/ their publication count by matching the IDs in the ID column of "authors_name_id_pub_count_unlisted" to those in the "Var1" column of "pub_count"
authors_name_id_pub_count_unlisted$pub_count <- id_pub_count$Freq[match(authors_name_id_pub_count_unlisted$id, id_pub_count$Var1)]
## sort the dataframe based on the publication count from highest to lowest
authors_ranked <- authors_name_id_pub_count_unlisted[order(authors_name_id_pub_count_unlisted$pub_count, decreasing = TRUE), ]
## get the top 5 most prolific authors
most_prolific_authors <- authors_ranked[1:9, ]
By creating an ID adjacency list and a names adjacency list, we can create a dataset where authors’ ID match their name. This enables us to account for instances where authors have changed their names. For example, if an author changed their name once, they’ll initially appear as two different authors. However, because their ID didn’t change, we can have them appear as one author and with the name they used in their most recent publication.
During the cleaning process, my adjacency lists for author names and IDs did have the same dimensions (421 x 31). The dimension means that there are 421 articles, with at least one of the articles having 31 authors. If the two adjacency lists didn’t have the same dimensions, some reasons for the discrepancy include authors who have suffixes such as Jr., II., or Ph.D at the end of their names. To address these issues, I would remove them from the authors’ names.
The number of rows in the database with the ID and name columns (1,316) means that there are 1,316 unique authors. This number doesn’t match the number of unique actors in the graph created in Deliverable 1, which was 1,263. I think this mismatch is due to the fact that the database with the ID and name columns accounts for solo authors, whereas the graph created in Deliverable 1 didn’t account for solo authors.
In this feature engineering process, unlisting is used to re-shape the authors’ name and ID adjacency lists from a wide format (multiple columns for authors’ names and IDs) to a long format (one column for authors’ name and another column for authors’ ID).
The top five most prolific authors in the dataset are as follows:
With 16 publications, the most prolific is Wanli Xing, who publishes about students’ STEM learning in online environments.
With 11 publications, the second most prolific is Kyle M. L. Jones, who publishes about students’ level of privacy and trust in relation to higher education’s educational data mining practices.
With 9 publications, the third most prolific is Ryan S. Baker, who publishes about learners’ engagement in Massive Open Online Courses (MOOCs), adaptive learning systems, and educational games.
With 7 publications, the following three authors are the fourth most prolific: José A. Ruipérez-Valiente, Charles Xie, and Martin V. Pusic. José A. Ruipérez-Valiente publishes about serious games and and cultural factors that influence students’ engagement in MOOCs. Charles Xie publishes about students’ self-regulation in computer-based, STEM learning environments. Martin V. Pusic publishes about the role of big data and learning analytics in medical/health education.
With 5 publications, the following three authors are the fifth most prolific: George Siemens, Xavier Ochoa, and Guanhua Chen. George Siemens publishes about connectivism and MOOCs. Xavier Ochoa publishes about learning analytics initiatives in Latin America. Guanhua Chen publishes about students’ self-regulation in computer-based, STEM learning environments, particularly focusing on engineering.
# link author IDs and article IDs
## use "authors_id_split" for author IDs and tranform it to a matrix
authors_id_split_matrix <- as.matrix(authors_id_split)
## create a combined dataframe w/ the IDs of authors and articles
combined_authors_articles_id <- cbind(scopus$EID, authors_id_split_matrix)
## transform the dataframe into a matrix
combined_authors_articles_id_matrix <- as.matrix(combined_authors_articles_id)
## create an edgelist w/ the article IDs as the 1st column and author IDs as the 2nd column - combine the 1st column with everything but itself
edgelist_two_mode <- cbind(combined_authors_articles_id_matrix[, 1], c(combined_authors_articles_id_matrix[, -1]))
## only keep rows where there isn't a NA in the author ID column
edgelist_two_mode <- edgelist_two_mode[!is.na(edgelist_two_mode[, 2]), ] ## dim: 1,535 x 2
## create a graph using the two mode edgelist
graph_two_mode <- graph.edgelist(edgelist_two_mode[, 2:1], directed = FALSE) ## returns: 1,737 1,535
## if there's a match btw the article ID in "edgelist_two_mode" and the attribute "name" in "graph_two_mode", then return TRUE; otherwise, return FALSE
V(graph_two_mode)$type <- V(graph_two_mode)$name %in% edgelist_two_mode[, 1]
# table(V(graph_two_mode)$type) ## FALSE: number of authors [column 2] (1,316); TRUE: number of articles [column 1] (421)
## create labels of author name based on their ID in the graph
V(graph_two_mode)$label <- authors_name_id_pub_count_unlisted$names[match(V(graph_two_mode)$name, authors_name_id_pub_count_unlisted$id)]
# table(is.na(V(graph_two_mode)$label)) ## FALSE: number of authors [column 2] (1,316); TRUE: number of articles [column 1] (421)
V(graph_two_mode)$label <- ifelse(is.na(V(graph_two_mode)$label), V(graph_two_mode)$name, V(graph_two_mode)$label)
# table(is.na(V(graph_two_mode)$label)) ## FALSE: 1,737 - there are no missing values for the label attribute
In the past two deliverables, since solo authors have no co-authors, they were connected with “NA” in the master edgelist. When we removed rows where the value of the second column was missing, rows with solo authors were excluded from the edgelist.
The transformations conducted in the R code file
two_mode_coauthorship.r handles solo authors by linking the
article ID with each author ID associated with it.
Our database with ID, names, and publication counts does contain solo authors. This was achieved by linking authors’ name in their most recent publication with their author ID.
The dimensions of the object edgelist_two_mode
(1,535 x 2) means that there are 1,535 connections in the
network.
There was a discrepancy in the distributions of True and False
values in table(is.na(V(g2)$label)) and
table(V(g2)Stype) due to a problem with the data collected
from Scopus. For six people, there was no name associated with an
author’s ID. After taking a closer look, we found that there were
articles that had multiple author IDs, but were missing one or more the
respective author name(s). We addressed this discrepancy with the
following line of code:
V(g2)$label <- ifelse(is.na(V(g2)$label), V(g2)$name, V(g2)$label).
This line of code states that if the label in the object g2
is missing, then the label will be the name in g2.
Otherwise, if the label isn’t missing, then the label will stay the
same.
All my authors had their names associated an ID, as indicated by
the same distributions of True and False values in
table(is.na(V(graph_two_mode)$label)) and
table(V(graph_two_mode)Stype), so I didn’t face a similar
problem with the one we had in class.
# transform two mode network from deliverable 3 into a one mode network
## retrieve the matrix of the two mode network (dim: 1,316 [authors] x 421 [articles])
mat_graph_two_mode_incidence <- get.incidence(graph_two_mode)
## create a one mode network of authors (square matrix w/ dim: 1,316 x 1,316)
mat_graph_one_mode <- mat_graph_two_mode_incidence %*% t(mat_graph_two_mode_incidence)
# summary(diag(mat_graph_one_mode)) ## returns: max = 16, which means that the highest number of publications is 16 - this aligns w/ the publication count of the most prolific author, which is 16
graph_one_mode <- graph.adjacency(mat_graph_one_mode, mode = "undirected", weighted = TRUE, diag = F)
# summary(E(graph_one_mode)$weight) ## returns: max = 5, which indicates that the highest number of co-publications btw authors is 5
# get measures of centrality
cent <- data.frame(bet = betweenness(graph_one_mode, normalized = TRUE, directed = FALSE)/max(betweenness(graph_one_mode, normalized = TRUE, directed = FALSE)), eig = evcent(graph_one_mode)$vector, degree = degree(graph_one_mode, mode = "total"))
# create a new column "name" (which is the authors' id)
cent$name <- rownames(cent)
# create a new column w/ the number of publications
cent$pub_count <- diag(mat_graph_one_mode)[match(cent$name, rownames(mat_graph_one_mode))]
# create new columns for the betweenness and size measures of centrality
cent$bet <- cent$bet/max(cent$bet)
cent$eig <- cent$eig/max(cent$eig)
# create a label that's a copy of name (aka the authors' ids) and replace name w/ a number, where counting starts at 1 and ends at 1,316
V(graph_one_mode)$label <- V(graph_one_mode)$name
V(graph_one_mode)$name <- 1:length(V(graph_one_mode))
# create a dataframe that contains the list of connections and the strength of these connections
links <- as.data.frame(cbind(get.edgelist(graph_one_mode), E(graph_one_mode)$weight))
# make the values of the links dataframe numeric
links$V1 <- as.numeric(as.character(links$V1))
links$V2 <- as.numeric(as.character(links$V2))
links$V3 <- round(as.numeric(as.character(links$V3)), 3)
# add column names to the links dataframe
colnames(links) <- c("source","target", "value")
# have counts start at 0
links[, 1:2] <- (links[, 1:2] - 1)
# add attribute of author's name, publication count, eig, and size to the one mode network
V(graph_one_mode)$author_name = authors_name_id_pub_count_unlisted$name[match(V(graph_one_mode)$label, authors_name_id_pub_count_unlisted$id)]
V(graph_one_mode)$pub_count = authors_name_id_pub_count_unlisted$pub_count[match(V(graph_one_mode)$label, authors_name_id_pub_count_unlisted$id)]
V(graph_one_mode)$eig <- cent$eig[match(V(graph_one_mode)$label, cent$name)]
V(graph_one_mode)$size <- cent$bet[match(V(graph_one_mode)$label, cent$name)]
# create a nodes dataframe
nodes <- data.frame(name = c(paste("Name: ", V(graph_one_mode)$author_name,
", ID: ", V(graph_one_mode)$label,
", No. of publications: ", V(graph_one_mode)$pub_count,
sep = "")),
pub_count = V(graph_one_mode)$pub_count,
EV_cent = abs(V(graph_one_mode)$eig),
size = abs(V(graph_one_mode)$size))
## make the name column in the nodes dataframe all character types
nodes$name <- as.character(nodes$name)
## create an empty group column in the nodes dataframe
nodes$group <- NA
# table(is.na(nodes$group)) ## checks for NAs in the group column - returns: F: 1,316, which shows that there are no NAs
## group authors based on their number of publications
nodes$group <- cut(nodes$pub_count, c(0, 1, 4, 7, max(nodes$pub_count)), right = TRUE, labels = FALSE)
## assign labels to each group
nodes$group <- ifelse(is.na(nodes$group), "N/A", ifelse(nodes$group == 1, "1 Publication", ifelse(nodes$group == 2, "2-4 Publications", ifelse(nodes$group == 3, "5-7 Publications", "8+ Publications"))))
# create a dataframe for the counts of groups
counts <- data.frame(table(nodes$group))
## create a column "labels" for the counts
counts$labels <- paste(counts$Var1, " (N = ", counts$Freq, ")", sep = "")
## add the "labels" column as a new column "groups" in the nodes dataframe
nodes$groups <- counts$labels[match(nodes$group, counts$Var1)]
# network visualization
library(networkD3)
library(magrittr)
library(htmlwidgets)
library(htmltools)
netviz <- forceNetwork(Links = links, Nodes = nodes,
Source = 'source', Target = 'target',
NodeID = 'name',
Group = 'groups',
charge = -30,
linkDistance = JS("function(d) { return d.linkDistance; }"), #JS("function(d){return d.value}"),
linkWidth = JS("function(d) { return Math.sqrt(d.value)*2; }"),
opacity = 0.8,
Value = "value",
Nodesize = "size",
radiusCalculation = JS("Math.sqrt(d.nodesize*30)+4"),
zoom = T,
fontSize = 14,
bounded = F,
legend = TRUE,
linkColour = ifelse(links$value == 1, "#ABB2B9", ifelse(links$value == 2, "#FF66FB", "#7966FF ")),
colourScale = JS("d3.scaleOrdinal(d3.schemeCategory10)"))
## HTML code
HTMLaddons <-
"function(el, x) {
d3.select('body').style('background-color', ' white ')
d3.selectAll('.legend text').style('fill', 'black')
d3.selectAll('.link').append('svg:title')
.text(function(d) { return 'No. of co-publications: ' + d.value ; })
var options = x.options;
var svg = d3.select(el).select('svg')
var node = svg.selectAll('.node');
var link = svg.selectAll('link');
var mouseout = d3.selectAll('.node').on('mouseout');
function nodeSize(d) {
if (options.nodesize) {
return eval(options.radiusCalculation);
} else {
return 6;
}
}
d3.selectAll('.node').on('click', onclick)
function onclick(d) {
if (d3.select(this).on('mouseout') == mouseout) {
d3.select(this).on('mouseout', mouseout_clicked);
} else {
d3.select(this).on('mouseout', mouseout);
}
}
function mouseout_clicked(d) {
node.style('opacity', +options.opacity);
link.style('opacity', +options.opacity);
d3.select(this).select('circle').transition()
.duration(750)
.attr('r', function(d){return nodeSize(d);});
d3.select(this).select('text').transition()
.duration(1250)
.attr('x', 0)
.style('font', options.fontSize + 'px ');
}
}
"
netviz$x$links$linkDistance <- (1/links$value) * 100
# render visualization
onRender(netviz, HTMLaddons)
I chose to start with a two mode network by linking authors to articles and then transform it into a one mode network to map the co-author relationships because I wanted to retain solo authors in my dataset.
The process of creating this interactive network visualization is important for interactive reviewability, as it allows readers to interact with the analyses we have conducted. For these deliverables, the visualization above displays an overview of the key authors, as highlighted by the size and color of their node, and the clusters that emerge from them. Readers can click on or hover over nodes to learn about an author’s name, ID, and their publication count and to see the other authors they have published with, if any. Readers can also hover over edges between two authors to learn about the number of co-publications they have.