Befor you practice the following codes, make sure you understand what the data look like and install the KoNLP package to analyse Korean texts.

rm(list = ls())  #you always want to start from empty and clearn place

Load necessary packages to play the game!

library(readxl)     # read Excel
library(dplyr)      # data wrangling
## 
## 다음의 패키지를 부착합니다: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(stringr)    # string processing
## Warning: 패키지 'stringr'는 R 버전 4.3.3에서 작성되었습니다
library(tibble)     # as_tibble()
library(tidytext)   # unnest_tokens()
library(tidyr)      # separate()
library(widyr)      # pairwise_count()
## Warning: 패키지 'widyr'는 R 버전 4.3.3에서 작성되었습니다
library(tidygraph)  # network data structure + metrics
## Warning: 패키지 'tidygraph'는 R 버전 4.3.3에서 작성되었습니다
## 
## 다음의 패키지를 부착합니다: 'tidygraph'
## The following object is masked from 'package:stats':
## 
##     filter
library(ggraph)     # network visualization
## Warning: 패키지 'ggraph'는 R 버전 4.3.3에서 작성되었습니다
## 필요한 패키지를 로딩중입니다: ggplot2
library(KoNLP)      # Korean NLP (extractNoun)
## Checking user defined dictionary!
library(showtext)
## Warning: 패키지 'showtext'는 R 버전 4.3.3에서 작성되었습니다
## 필요한 패키지를 로딩중입니다: sysfonts
## Warning: 패키지 'sysfonts'는 R 버전 4.3.3에서 작성되었습니다
## 필요한 패키지를 로딩중입니다: showtextdb
## Warning: 패키지 'showtextdb'는 R 버전 4.3.3에서 작성되었습니다

Import data that you want to extract text data from

setwd("/Users/se776257/OneDrive - University of Central Florida/Desktop/Prof. An/02 Teaching/Guest lecture/2026 Kyungpook/Method workshop/")

#R can import different data files including CSV, Stata, and many others. Make sure you use the correct code to import your data. In this case, we need 'readxl' to import an Excel file
df  <- read_excel("ngonpo_1347.xlsx")

#Let's extract text data (column) from your raw data
txt <- df$Title

First, I want to see what terms are frequently used in Korean literature to name ‘nonprofit sector’

Sub1 <- df[grep("비영리",   df$Title), ]; nrow(Sub1)  # nonprofit
## [1] 44
Sub2 <- df[grep("비정부",   df$Title), ]; nrow(Sub2)  # NGO (Korean)
## [1] 11
Sub3 <- df[grep("3섹터",    df$Title), ]; nrow(Sub3)  # third sector
## [1] 0
Sub4 <- df[grep("시민사회", df$Title), ]; nrow(Sub4)  # civil society
## [1] 69
Sub5 <- df[grep("NGO", df$Title), ]; nrow(Sub5)       # NGO (uppercase)
## [1] 410
Sub6 <- df[grep("ngo", df$Title), ]; nrow(Sub6)       # NGO (lowercase)
## [1] 0

Text data cleaning is the most important stage just like other quant data.

Sys.setlocale("LC_ALL", "English") #R is English based programming program, Korean language sometimes cause errors. We do our analysis on the English field, and then will see the results on the Korean field
## Warning in Sys.setlocale("LC_ALL", "English"): using locale code page other
## than 65001 ("UTF-8") may cause problems
## [1] "LC_COLLATE=English_United States.1252;LC_CTYPE=English_United States.1252;LC_MONETARY=English_United States.1252;LC_NUMERIC=C;LC_TIME=English_United States.1252"
texts <- txt %>%
  str_replace_all("\r", "") %>%
  str_replace_all("\n", " ") %>%
  str_replace_all("[[:punct:]]", " ") %>%
  str_replace_all("[^가-힣]", " ") %>%  # keep Korean syllables only
  str_replace_all("/", " ") %>%
  str_trim(side = "both") %>%
  str_squish()

texts <- as_tibble(texts)  # creates a tibble with column name `value`

Sys.setlocale("LC_ALL")  #Back to Kroean field to see the results
## [1] "LC_COLLATE=Korean_Korea.utf8;LC_CTYPE=Korean_Korea.utf8;LC_MONETARY=Korean_Korea.utf8;LC_NUMERIC=C;LC_TIME=Korean_Korea.utf8"
#Let's see the first 10 rows
texts %>%
  head(10)
## # A tibble: 10 × 1
##    value                                                                        
##    <chr>                                                                        
##  1 북한식 민간단체의 현황과 변화 전망 남북 교류 협력의 관점에서                 
##  2 연구 문화담론에 대한 비판적 고찰 민중항쟁에서 아시아문화중심도시 에 이르기까…
##  3 특집 환경정의 경험적 연구 사례를 통해 본 대만과 한국에서의 환경정의 문제 환… 
##  4 포럼 시민운동의 정치참여 가능성은 한국 시민운동의 정치세력화 어떻게 가능할까 
##  5 녹색도시 의 개념 정립과 실현 방향 설정을 위한 연구                           
##  6 대북협력민간단체협의회 약칭 북민협 의 네트워크 구조 및 동학                  
##  7 대북협력민간단체협의회 약칭 북민협 의 네트워크 구조 및 동학 한국정부와의 관… 
##  8 사회 의 위기와 사회적인 것 의 범람 한국과 중국의 사회건설 프로젝트에 관한 소…
##  9 시민의견광고운동 을 통해 본 일본의 진보적 시민운동                           
## 10 총체적 선교 관점에서 본 국내 기독 의 지역개발사업 평가와 발전방안 제시 한국…

Let’s see what Korean (single) words were most used in the nonprofit studies

#First, we only extract nouns
word_noun <- texts %>%
  unnest_tokens(output = word,
                input  = value,
                token  = extractNoun) %>%
  filter(str_count(word) > 1) %>%  # remove 1-character tokens
  mutate(
    word = ifelse(str_detect(word, "한국의"), "한국", word),  #I added them after checking the results of the first round. You may want to check your auto analysis to manually clean your data.
    word = ifelse(str_detect(word, "비정"),   "비정부", word)
  ) %>%
  count(word, sort = TRUE) %>%
  filter(!str_detect(word,
                     "연구|사례|논문|분석|중심|현황|활동|비정부|특집|관계|역할|영역|영향"))

Top 20 words

word_noun %>% head(20) 
## # A tibble: 20 × 2
##    word         n
##    <chr>    <int>
##  1 시민       212
##  2 사회       192
##  3 정부       137
##  4 한국       122
##  5 거버넌스    93
##  6 단체        80
##  7 참여        80
##  8 개발        76
##  9 국제        74
## 10 조직        73
## 11 지원        70
## 12 지방        66
## 13 여성        65
## 14 정책        65
## 15 환경        64
## 16 협력        63
## 17 지역        57
## 18 기업        56
## 19 운동        55
## 20 정치        49

You may want to try TF-IDF approach too

The single words frequency delivers limited information regarding topics in the document (data). Now we want to see two word pairs

# Co-occurrence (pairwise within each title)
# =====================================================
Sys.setlocale("LC_ALL", "English")
## Warning in Sys.setlocale("LC_ALL", "English"): using locale code page other
## than 65001 ("UTF-8") may cause problems
## [1] "LC_COLLATE=English_United States.1252;LC_CTYPE=English_United States.1252;LC_MONETARY=English_United States.1252;LC_NUMERIC=C;LC_TIME=English_United States.1252"
texts <- texts %>%
  mutate(id = row_number())

texts_pos <- texts %>%
  unnest_tokens(output = word,
                input  = value,
                token  = extractNoun)

pumsa <- texts_pos %>%
  filter(str_count(word) >= 2) %>%
  arrange(id) %>%
  mutate(
    word = ifelse(str_detect(word, "한국의"), "한국", word),
    word = ifelse(str_detect(word, "비정"),   "비정부", word)
  ) %>%
  filter(!str_detect(word,
                     "비영리|시민|사회|연구|논문|중심|활동|현황|비정부|비정|부단|매개|효과|특집|영역|영향|요인"))

This shows two words that apear together in the same document

pair <- pumsa %>%
  pairwise_count(item = word,
                 feature = id,
                 sort = TRUE)
Sys.setlocale("LC_ALL")
## [1] "LC_COLLATE=Korean_Korea.utf8;LC_CTYPE=Korean_Korea.utf8;LC_MONETARY=Korean_Korea.utf8;LC_NUMERIC=C;LC_TIME=Korean_Korea.utf8"
pair %>% head(20)
## # A tibble: 20 × 3
##    item1    item2        n
##    <chr>    <chr>    <dbl>
##  1 지방     정부        60
##  2 정부     지방        60
##  3 관계     정부        31
##  4 정부     관계        31
##  5 국제     협력        20
##  6 개발     협력        20
##  7 협력     국제        20
##  8 개발     국제        20
##  9 협력     개발        20
## 10 국제     개발        20
## 11 로컬     거버넌스    20
## 12 거버넌스 로컬        20
## 13 정부     협력        17
## 14 거버넌스 사례        17
## 15 협력     정부        17
## 16 분석     정부        17
## 17 파트너십 정부        17
## 18 정부     분석        17
## 19 정부     파트너십    17
## 20 사례     거버넌스    17

Now, I want to go stricter: I want to see the two words attached in the document

# Bigrams (noun-noun sequences within each title)
line_pumsa <- pumsa %>%
  group_by(id) %>%
  summarize(sentence = paste(word, collapse = " "), .groups = "drop")

bigram_text <- line_pumsa %>%
  unnest_tokens(output = bigram,
                input  = sentence,
                token  = "ngrams",
                n      = 2)

bigram_split <- bigram_text %>%
  separate(bigram, c("word1", "word2"), sep = " ") %>%
  filter(word1 != word2)

pair_bigram <- bigram_split %>%
  count(word1, word2, sort = TRUE) %>%
  na.omit()

Let’s see the results and compare this with pairwise (co-accurence) approach

# Top 20 bigrams (table)
pair_bigram %>% head(20)
## # A tibble: 20 × 3
##    word1    word2        n
##    <chr>    <chr>    <int>
##  1 지방     정부        60
##  2 정부     관계        22
##  3 로컬     거버넌스    20
##  4 국제     개발        19
##  5 개발     협력        16
##  6 정부     파트너십    15
##  7 정책     과정        14
##  8 거버넌스 형성        10
##  9 교류     협력        10
## 10 북한     이탈        10
## 11 글로벌   거버넌스     9
## 12 이주     노동자       9
## 13 이탈     주민         9
## 14 과정     역할         8
## 15 기업     책임         8
## 16 정부     협력         8
## 17 지원     사업         8
## 18 활성화   방안         8
## 19 거버넌스 구축         7
## 20 북한     인권문제     7

Now, you may have some words that you want to further remove from the analyses to more pricisely analyze topics. This process is NOT on the machine BUT on the researchers. Feel free to clean more your data and run the above analyses to get better results

Network graph from bigrams + visualization

set.seed(1234)

graph_bigram <- pair_bigram %>%
  slice_max(n, n = 50, with_ties = FALSE) %>%
  as_tbl_graph(directed = FALSE) %>%
  mutate(
    centrality = centrality_degree(),
    group      = as.factor(group_infomap())
  )

graph_bigram
## # A tbl_graph: 62 nodes and 50 edges
## #
## # An undirected simple graph with 14 components
## #
## # Node Data: 62 × 3 (active)
##    name     centrality group
##    <chr>         <dbl> <fct>
##  1 지방              1 5    
##  2 정부              6 5    
##  3 로컬              1 3    
##  4 국제              3 1    
##  5 개발              3 1    
##  6 정책              1 4    
##  7 거버넌스          5 3    
##  8 교류              1 1    
##  9 북한              2 6    
## 10 글로벌            1 3    
## # ℹ 52 more rows
## #
## # Edge Data: 50 × 3
##    from    to     n
##   <int> <int> <int>
## 1     1     2    60
## 2     2    29    22
## 3     3     7    20
## # ℹ 47 more rows

Let’s draw a network of the selected bigrams

#If you do not have the font
font_add(family = "Malgun Gothic", regular = "malgun.ttf")
showtext_auto()

ggraph(graph_bigram, layout = "fr") +
  geom_edge_link(color = "gray50", alpha = 0.5) +
  geom_node_point(aes(size = centrality, color = group), show.legend = FALSE) +
  scale_size(range = c(5, 15)) +
  geom_node_text(aes(label = name),
                 repel = TRUE,
                 family = "Malgun Gothic",
                 show.legend = FALSE) +
  theme_graph()