Abstract
This document outlines the methodology used for a quantitative analysis of Chinese scholarship on study abroad, retrieved from CNKI in early September 2024. It begins by introducing the data, then examines the metadata, and finally applies various text analysis techniques—tf-idf, biterm topic modeling, and named entity recognition (NER)—to analyze the content of titles..
This document presents the methodology used for a quantitative analysis of Chinese scholarship on study abroad, as available on the China National Knowledge Infrastructure (CNKI), the primary database for Chinese scholarly literature today. The query was conducted on September 1, 2024, using the keyword 留学 (liuxue) in both the “Title” and “Subject” fields, yielding 2,067 results spanning the period from 1957 to 2024. We analyze these results in detail below.
In the first step, we load the list of articles along with related metadata retrieved from CNKI. The dataset comprises 2,067 rows, representing the scholarly works, and 13 columns, corresponding to the metadata variables provided by CNKI. Details of these metadata variables are presented below. The table below displays the first 10 rows, ordered by date of publication.
library(readr)
cnki_liuxue <- read_csv("cnki_liuxue.csv",
col_types = cols(...1 = col_skip()))
cnki_liuxue %>% arrange(PubTime)
Detailed Description of Variables:
Note: For simplicity, the term “article” is used throughout the document to refer broadly to all scholarly works, encompassing journal articles, theses, conference papers, and other scholarly outputs.
library(hrbrthemes)
# Histogram Plot
cnki_liuxue %>%
ggplot( aes(x=Year)) +
geom_histogram( binwidth=1, fill="#69b3a2", color="#e9ecef", alpha=0.9) +
ggtitle("Chinese Scholarship on 留学") +
theme_ipsum() +
theme(
plot.title = element_text(size=15)
) + labs(caption = "Based on CNKI")
# Black & White Plot for TCC
cnki_liuxue %>%
ggplot(aes(x = Year)) +
geom_histogram(binwidth = 1, fill = "grey", color = "black", alpha = 0.9) +
ggtitle("Chinese Scholarship on 留学") +
theme_ipsum() +
theme(
plot.title = element_text(size = 15),
plot.caption = element_text(size = 10),
axis.text = element_text(color = "black"), # Ensure axis text is black
axis.title = element_text(color = "black"), # Ensure axis title is black
panel.grid.major = element_line(color = "black"), # Major grid lines in black
panel.grid.minor = element_line(color = "black") # Minor grid lines in black
) +
labs(caption = "Based on CNKI")
cnki_liuxue$Period <- cut(cnki_liuxue$Year, c(1957, 1990, 2003, 2015, 2024), include.lowest = TRUE, right = FALSE,
labels = c("1957-1989", "1990-2002", "2003-2014", "2015-2024"))
cnki_liuxue %>% group_by(Period) %>% count() %>% mutate(percent = round(n/2067*100, 1))
cnki_liuxue$Decade <- paste0(substr(cnki_liuxue$Year, 0, 3), "0")
cnki_liuxue <- cnki_liuxue %>% relocate(Decade, .after = Year)
cnki_liuxue %>% group_by(Decade) %>% count() %>% mutate(percent = round(n/2067*100, 1))
cnki_liuxue %>% group_by(SrcDatabase) %>% count(sort = TRUE) %>% mutate(percent = round(n/2067*100, 1))
### Broader Categorization
cnki_liuxue <- cnki_liuxue %>%
mutate(type = fct_collapse(SrcDatabase,
thesis = c("硕士", "博士"),
journal = c("辑刊", "期刊"),
newspaper = c("报纸"),
conference = c("国际会议", "中国会议")
)) %>% relocate(type, .after = SrcDatabase)
cnki_liuxue %>% group_by(type) %>% count(sort = TRUE) %>% mutate(percent = round(n/2067*100, 1))
cnki_liuxue %>% group_by(Period, type) %>% count() %>% arrange(Period)
cnki_liuxue %>% group_by(Period, type) %>% count() %>% arrange(type)
cnki_liuxue %>%
mutate(source = str_remove(Literature.Source, "\\s*\\(.*\\)")) %>%
group_by(source) %>% count(sort = TRUE)
cnki_liuxue_affiliation <- cnki_liuxue %>% separate_rows(Affiliation, sep = "; ") %>%
separate_rows(Affiliation, sep = "、 ") %>%
separate_rows(Affiliation, sep = ", ")%>%
mutate(Affiliation = str_remove(Affiliation, "\\s*\\(.*\\)")) %>%
mutate(Affiliation = str_replace(Affiliation, ";", "")) %>%
mutate(Affiliation = str_replace(Affiliation, ";", "")) %>%
mutate(Affiliation = str_replace(Affiliation, ";", "")) %>%
mutate(Affiliation = str_replace(Affiliation, "、", "")) %>%
mutate(Affiliation = str_replace(Affiliation, ",", ""))%>%
mutate(Affiliation = str_remove_all(Affiliation, "\\d")) %>%
mutate(Affiliation = str_replace(Affiliation, " ", "")) %>%
mutate(length = nchar(Affiliation)) %>% filter(length > 2)
cnki_liuxue_affiliation %>% group_by(Affiliation) %>% count(sort = TRUE)
cnki_liuxue_affiliation %>% filter(type == "thesis") %>% group_by(Affiliation) %>% count(sort = TRUE) # all theses
cnki_liuxue_affiliation %>% filter(SrcDatabase == "博士") %>% group_by(Affiliation) %>% count(sort = TRUE) # doctoral dissertations only
cnki_liuxue_affiliation %>% filter(type == "conference") %>% group_by(Affiliation) %>% count(sort = TRUE)
In this section, we aim to analyze the content of the scholarship in greater depth, identifying key concepts and examining how research topics have shifted over time. Since abstracts are not consistently provided and are sometimes incomplete, we chose to rely on the titles of the articles. While this approach is reductive and does not fully capture the entirety of the article content, it offers the most systematic information available, allowing us to include all scholarly works, even those without abstracts.
The first step is to perform tokenization on the titles, that, to segment Chinese titles into words. For this prupose, we use (jiebaR)[https://github.com/qinwf/jiebaR], one of the most popular R package for Chinese word segmentation, which performs well on contemporary Chinese:
The first step is to perform tokenization on the titles, which involves segmenting Chinese titles into individual words. For this purpose, we use (jiebaR)[https://github.com/qinwf/jiebaR], one of the most popular packages for Chinese word segmentation, which is particularly effective with contemporary Chinese:
library(jiebaR)
cnki_titles <- cnki_liuxue
# Initialize jiebaR worker
cutter <- worker()
# define the segmentation function
seg_x <- function(x) {str_c(cutter[x], collapse = " ")}
# apply the function to documents
x.out <- sapply(cnki_titles$Title, seg_x, USE.NAMES = FALSE)
# Attach the segmented text back to the data frame
cnki_titles$Title.seg <- x.out
cnki_titles <- cnki_titles %>% relocate(Title.seg, .after = Title)
# Count number of tokens in each title
library(quanteda)
library(quanteda.textstats)
cnki_tokenized <- cnki_titles %>%
mutate(ntoken = ntoken(Title.seg))
# Unnest tokens (split titles into as many rows as tokens they contain)
library(tidytext)
cnki_tokens <- cnki_tokenized %>%
unnest_tokens(output = token,
input = Title.seg,
token = stringr::str_split,
pattern = " ") # 16,217 observations
# Count number of occurrences for each token
cnki_token_count <- cnki_tokens %>% group_by(token) %>% count() %>% mutate(lgth = nchar(token)) # 3,389 unique tokens
# Remove non-Chinese tokens (numbers, punctuation) and retain only tokens with two or more characters
cnki_token_filtered <- cnki_token_count %>% filter(lgth >1) %>%
mutate(token = str_replace_all(token, "[:digit:]", "")) %>%
mutate(token = str_replace_all(token, "[[:punct:]]", " "))
cnki_token_filtered[cnki_token_filtered==""]<-NA
cnki_token_filtered[cnki_token_filtered==" "]<-NA
cnki_token_filtered[cnki_token_filtered==" "]<-NA
cnki_token_filtered <- cnki_token_filtered %>%
drop_na(token)
# 2,840 unique tokens remain
# Filter out irrelevant tokens from the dataset of tokenized titles (16,217 observations remain)
cnki_titles_filtered <- cnki_tokens %>% filter(token %in% c(cnki_token_filtered$token))
# Most frequent tokens
cnki_titles_filtered %>% group_by(token) %>% count(sort = TRUE)
To improve the quality of our text analysis, we need to remove stopwords—tokens that are overly frequent in the Chinese language in general and within the specific context of scholarship and the topic of study abroad. For example, we will eliminate terms like “中国” (Zhongguo, China) and the keywords used in the initial query (留学, liuxue), as well as common scholarly terms such as “研究” (yanjiu, research).
For general stopwords, we utilize dictionaries created by previous users, such as those available through the stopwords R package. For customized stopwords, we employ a carefully curated list based on the frequency of words extracted from this dataset.
library(stopwords)
# list existing dictionaries and languages: 3 sources for Chinese:
stopwords::stopwords_getsources()
## [1] "snowball" "stopwords-iso" "misc" "smart"
## [5] "marimo" "ancient" "nltk" "perseus"
stopwords::stopwords_getlanguages("marimo") # 2 types of Chinese : zh_tw (Taiwan) and zh_cn (mainland Chinese)
## [1] "en" "de" "ru" "ar" "he" "zh_tw" "zh_cn" "ko" "ja"
stopwords::stopwords_getlanguages("stopwords-iso")
## [1] "af" "ar" "hy" "eu" "bn" "br" "bg" "ca" "zh" "hr" "cs" "da" "nl" "en" "eo"
## [16] "et" "fi" "fr" "gl" "de" "el" "ha" "he" "hi" "hu" "id" "ga" "it" "ja" "ko"
## [31] "ku" "la" "lt" "lv" "ms" "mr" "no" "fa" "pl" "pt" "ro" "ru" "sk" "sl" "so"
## [46] "st" "es" "sw" "sv" "th" "tl" "tr" "uk" "ur" "vi" "yo" "zu"
stopwords::stopwords_getlanguages("misc")
## [1] "ar" "ca" "el" "gu" "zh"
# extract stopwords from different dictionaries
zh_iso_stopwords <- stopwords(language = "zh", source = "stopwords-iso")
zh_marimo_tw <- stopwords(language = "zh_tw", source = "marimo")
zh_misc <- stopwords(language = "zh", source = "misc")
stop_iso <- as.data.frame(zh_iso_stopwords)
stop_iso <- stop_iso %>% mutate(word = zh_iso_stopwords)
stop_iso$zh_iso_stopwords <- NULL
stop_marimo <- as.data.frame(zh_marimo_tw)
stop_marimo <- stop_marimo %>% mutate(word = zh_marimo_tw)
stop_marimo$zh_marimo_tw <- NULL
stop_misc <- as.data.frame(zh_misc)
stop_misc <- stop_misc %>% mutate(word = zh_misc)
stop_misc$zh_misc <- NULL
# Combine stopwords from different sources
all_stop_words <- bind_rows(stop_iso, stop_marimo, stop_misc) %>% unique()
# Remove stop words from our dataset of tokenized titles
cnki_token_filtered <- cnki_token_filtered %>% filter(!token %in% all_stop_words$word) # 2,807 unique tokens remain
cnki_titles_filtered <- cnki_titles_filtered %>% filter(!token %in% all_stop_words$word) # 12,057 titles remain
library(readr)
cnki_title_stopword_list <- read_csv("cnki_title_stopword_list.csv",
col_types = cols(...1 = col_skip()))
cnki_token_filtered <- cnki_token_filtered %>% filter(!token %in% cnki_title_stopword_list$token) # 2,764 tokens remain
cnki_titles_filtered <- cnki_titles_filtered %>% filter(!token %in% cnki_title_stopword_list$token) # 9,679 titles remain
In the next step, we use Term Frequency-Inverse Document Frequency (TF-IDF) by period to identify the key terms for each period identified above TF-IDF is a statistical measure used to evaluate the importance of a word in a document relative to a collection of documents. It considers how frequently the word appears in the document compared to its overall frequency across the corpus.
The plots below display the seven most frequent words for each period:
cnki_period_tf_idf <- cnki_titles_filtered %>%
count(Period, token) %>%
bind_tf_idf(token, Period, n) %>%
arrange(desc(tf_idf))
cnki_period_tf_idf %>%
group_by(Period) %>%
top_n(7, tf_idf) %>%
ungroup() %>%
mutate(token = reorder(token, tf_idf)) %>%
ggplot(aes(tf_idf, token, fill = Period)) +
geom_col(show.legend = FALSE) +
facet_wrap(~ Period, scales = "free", nrow = 3) +
labs(x = "tf-idf", y = "token",
title = "Highest tf-idf words in CNKI titles",
subtitle = "tf-idf by period",
caption = "Source: CNKI (1957-2024)")
# Black and white plot for TCC
cnki_period_tf_idf %>%
group_by(Period) %>%
top_n(7, tf_idf) %>%
ungroup() %>%
mutate(token = reorder(token, tf_idf)) %>%
ggplot(aes(tf_idf, token)) + # Remove fill aesthetic
geom_col(aes(fill = Period), show.legend = FALSE) + # Optional if you want different shades
facet_wrap(~ Period, scales = "free", nrow = 3) +
labs(x = "tf-idf", y = "token",
title = "Highest tf-idf words in CNKI titles",
subtitle = "tf-idf by period",
caption = "Source: CNKI (1957-2024)") +
theme_bw(base_size = 12) + # Use a black-and-white theme
scale_fill_grey() # Use grey scale for the fill if desired
To inspect the representative articles associated with each of the
top words for Period 3, for instance, you can use the following line of
code:
p3_top <- cnki_period_tf_idf %>%
filter(Period == "2003-2014") %>% group_by(Period) %>%
top_n(7, tf_idf)
p3_top_articles <- cnki_titles_filtered %>%
filter(Period == "2003-2014") %>% filter(token %in% p3_top$token)
p3_top_articles %>% distinct(id, SrcDatabase, Title, Author)
We can use the term “比较” (bijiao, compare) to identify comparative studies:
cnki_comparative <- cnki_titles_filtered %>% filter(token == "比较") %>% unique() # 30 comparative studies
cnki_comparative
In the next step, we build a co-occurrence network to examine the words that most often appear together in article titles and identify the most central terms in this semantic constellation. We use pairwise counting to compute the strength of ties between pairs of words. Pairwise counting indicates how often specific words appear in the same title, providing insights into their co-occurrence patterns and potential semantic relationships.
## Compute pairwise count
library(widyr)
word_pairs <- cnki_titles_filtered %>%
pairwise_count(token, id, sort = TRUE)
# Create the co-occurrence network
set.seed(2024)
library(igraph)
library(tidygraph)
library(ggraph)
word_pairs %>%
filter(n > 5) %>%
graph_from_data_frame() %>%
{
# Calculate betweenness centrality
betweenness_centrality <- betweenness(., normalized = TRUE)
# Add betweenness centrality as a node attribute
V(.)$betweenness <- betweenness_centrality
# Create the graph with betweenness-based node sizes and label sizes
ggraph(., layout = "fr") +
geom_edge_link(aes(edge_alpha = n), show.legend = FALSE) +
geom_node_point(aes(size = betweenness), color = "orange") + # Use betweenness for node size
geom_node_text(aes(label = name, size = betweenness),
repel = TRUE,
point.padding = unit(0.2, "lines")) + # Use betweenness for label size
theme_void() +
labs(title = "Word co-occurrences in article titles focused on 留学",
subtitle = "Most frequent pairs (n > 5)",
caption = "Source: CNKI") +
scale_size_continuous(range = c(2, 8)) + # Adjust the size range for nodes
scale_size_continuous(name = "Betweenness Centrality", range = c(2, 8)) # Adjust the size range for labels
}
In the graph above, the size of each node is proportionate to its
betweenness centrality. In the analysis of
co-occurrence networks, betweenness centrality helps identify words or
terms that serve as central points of connection, highlighting their
significance in the semantic structure (in this particular context, the
scholarship on study abroad) and the relationships between different
concepts.
To facilitate the exploration and contextualization of semantic relations, it is helpful to create a two-mode network that links words to the titles in which they appear, along with their associated metadata. A two-mode network is a type of network that represents relationships between two distinct sets of entities — such as, in this context, words and the titles in which they appear.
# create edge list linking words (tokens) and article titles
edge_titles <- cnki_titles_filtered %>% select(id, token)
edge_titles <- edge_titles %>% unique()
# create node list to differentiate token and titles, and list of attributes (metadata) for articles
title_node <- edge_titles %>% select(id) %>% mutate(type = "Title") %>% unique() %>% rename(name = id)
word_node <- edge_titles %>% select(token) %>% mutate(type = "Word") %>% unique() %>% rename(name = token)
title_node <- title_node %>% mutate(name = as.character(name))
node_titles <- bind_rows(title_node, word_node)
node_titles <- node_titles %>% unique()
# create attribute list for title nodes (article metadata)
node_attributes <- cnki_titles_filtered %>% distinct(id, SrcDatabase, type, Title, Author, Affiliation, Literature.Source, Year, Period)
# export the edge and node lists to project in a network analysis software, such as Gephi or Cytoscape
# write.csv(edge_titles, "edge_titles.csv")
# write.csv(node_titles, "node_titles.csv")
# write.csv(node_attributes, "node_attribute.csv")
This section relies on a more sophisticated method called topic modeling to identify key topics in the scholarship based on word co-occurrences in titles. Topic modeling is a natural language processing technique used to automatically identify and extract themes or topics from a collection of documents by analyzing word co-occurrences and patterns, typically represented in a probabilistic framework. In this study, we specifically utilized biterm topic modeling (BTM), which focuses on pairs of words and is particularly suitable for short texts like article titles. In the first step, we build different models for the entire corpus, with the number of topics ranging from 5 to 50 to enable different levels of granularity. In the second step, we construct topic models for each period to study how the topical focus changes over time.
# Not run here
library(BTM)
x <- cnki_titles_filtered %>% select(id, token)
set.seed(2024)
model5 <- BTM(x, k = 5, beta = 0.01, iter = 1000, trace = 100)
model10 <- BTM(x, k = 10, beta = 0.01, iter = 1000, trace = 100)
model15 <- BTM(x, k = 15, beta = 0.01, iter = 1000, trace = 100)
model20 <- BTM(x, k = 20, beta = 0.01, iter = 1000, trace = 100)
model25 <- BTM(x, k = 25, beta = 0.01, iter = 1000, trace = 100)
model50 <- BTM(x, k = 50, beta = 0.01, iter = 1000, trace = 100)
# Create dataset for each period
x1 <- cnki_titles_filtered %>% filter(Period == "1957-1989") %>% select(id, token)
x2 <- cnki_titles_filtered %>% filter(Period == "1990-2002") %>% select(id, token)
x3 <- cnki_titles_filtered %>% filter(Period == "2003-2014") %>% select(id, token)
x4 <- cnki_titles_filtered %>% filter(Period == "2015-2024") %>% select(id, token)
library(BTM)
set.seed(2024)
modelp1 <- BTM(x1, k = 5, beta = 0.01, iter = 1000, trace = 100)
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 1/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 101/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 201/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 301/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 401/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 501/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 601/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 701/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 801/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 901/1000
modelp2 <- BTM(x2, k = 10, beta = 0.01, iter = 1000, trace = 100)
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 1/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 101/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 201/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 301/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 401/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 501/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 601/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 701/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 801/1000
## 2024-10-26 21:10:57 Start Gibbs sampling iteration 901/1000
modelp3 <- BTM(x3, k = 20, beta = 0.01, iter = 1000, trace = 100)
## 2024-10-26 21:10:58 Start Gibbs sampling iteration 1/1000
## 2024-10-26 21:10:58 Start Gibbs sampling iteration 101/1000
## 2024-10-26 21:10:58 Start Gibbs sampling iteration 201/1000
## 2024-10-26 21:10:58 Start Gibbs sampling iteration 301/1000
## 2024-10-26 21:10:59 Start Gibbs sampling iteration 401/1000
## 2024-10-26 21:10:59 Start Gibbs sampling iteration 501/1000
## 2024-10-26 21:10:59 Start Gibbs sampling iteration 601/1000
## 2024-10-26 21:11:00 Start Gibbs sampling iteration 701/1000
## 2024-10-26 21:11:00 Start Gibbs sampling iteration 801/1000
## 2024-10-26 21:11:00 Start Gibbs sampling iteration 901/1000
modelp4 <- BTM(x4, k = 15, beta = 0.01, iter = 1000, trace = 100)
## 2024-10-26 21:11:01 Start Gibbs sampling iteration 1/1000
## 2024-10-26 21:11:01 Start Gibbs sampling iteration 101/1000
## 2024-10-26 21:11:01 Start Gibbs sampling iteration 201/1000
## 2024-10-26 21:11:01 Start Gibbs sampling iteration 301/1000
## 2024-10-26 21:11:02 Start Gibbs sampling iteration 401/1000
## 2024-10-26 21:11:02 Start Gibbs sampling iteration 501/1000
## 2024-10-26 21:11:02 Start Gibbs sampling iteration 601/1000
## 2024-10-26 21:11:02 Start Gibbs sampling iteration 701/1000
## 2024-10-26 21:11:02 Start Gibbs sampling iteration 801/1000
## 2024-10-26 21:11:02 Start Gibbs sampling iteration 901/1000
# Plot the topics
library(textplot)
library(ggraph)
library(concaveman)
# Period 1
plot(modelp1, top_n = 10,
title = "CNKI articles on 留学 (1957-1989)",
subtitle = "Biterm topic model with 5 topics") +
theme_minimal(base_size = 12) +
labs(edge_color = "Topic", # Change label for edge color to "Topic"
edge_alpha = "Cooccurrence Strength",
edge_width = "Cooccurrence Strength",
size = "Word Probability",
group = "Topic",
fill = "Topic", # Change label for edge width to "Cooccurrence Strength"
title = "CNKI articles on 留学 (1957-1989)",
subtitle = "Biterm topic model with 5 topics") +
theme(plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(face = "italic", size = 10),
axis.title = element_blank(), # Remove axis titles
axis.text = element_blank(),
panel.background = element_rect(fill = "white"), # Remove axis text
axis.ticks = element_blank()) + # Remove axis ticks
guides(edge_color = "none")
# Period 2
plot(modelp2, top_n = 10,
title = "CNKI articles on 留学 (1990-2002)",
subtitle = "Biterm topic model with 10 topics") +
theme_minimal(base_size = 12) +
labs(edge_color = "Topic", # Change label for edge color to "Topic"
edge_alpha = "Cooccurrence Strength",
edge_width = "Cooccurrence Strength",
size = "Word Probability",
group = "Topic",
fill = "Topic", # Change label for edge width to "Cooccurrence Strength"
title = "CNKI articles on 留学 (1990-2002)",
subtitle = "Biterm topic model with 10 topics") +
theme(plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(face = "italic", size = 10),
axis.title = element_blank(), # Remove axis titles
axis.text = element_blank(),
panel.background = element_rect(fill = "white"), # Remove axis text
axis.ticks = element_blank()) + # Remove axis ticks
guides(edge_color = "none",
fill = guide_legend(ncol = 2))