Last updated: 2023-11-02

Checks: 7 0

Knit directory: muse/

This reproducible R Markdown analysis was created with workflowr (version 1.7.1). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.


Great! Since the R Markdown file has been committed to the Git repository, you know the exact version of the code that produced these results.

Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.

The command set.seed(20200712) was run prior to running the code in the R Markdown file. Setting a seed ensures that any results that rely on randomness, e.g. subsampling or permutations, are reproducible.

Great job! Recording the operating system, R version, and package versions is critical for reproducibility.

Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.

Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.

Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.

The results in this page were generated with repository version e8b8d76. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.

Note that you need to be careful to ensure that all relevant files for the analysis have been committed to Git prior to generating the results (you can use wflow_publish or wflow_git_commit). workflowr only checks the R Markdown file, but you know if there are other scripts or data files that it depends on. Below is the status of the Git repository when the results were generated:


Ignored files:
    Ignored:    .Rhistory
    Ignored:    .Rproj.user/
    Ignored:    analysis/cbioportal_cache/
    Ignored:    r_packages_4.3.2/

Untracked files:
    Untracked:  analysis/cell_ranger.Rmd
    Untracked:  analysis/sleuth.Rmd
    Untracked:  analysis/tss_xgboost.Rmd
    Untracked:  code/multiz100way/
    Untracked:  data/HG00702_SH089_CHSTrio.chr1.vcf.gz
    Untracked:  data/HG00702_SH089_CHSTrio.chr1.vcf.gz.tbi
    Untracked:  data/ncrna_NONCODE[v3.0].fasta.tar.gz
    Untracked:  data/ncrna_noncode_v3.fa
    Untracked:  data/netmhciipan.out.gz
    Untracked:  export/davetang039sblog.WordPress.2023-06-30.xml
    Untracked:  export/output/
    Untracked:  women.json

Unstaged changes:
    Modified:   analysis/graph.Rmd
    Modified:   analysis/gsva.Rmd

Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.


These are the previous versions of the repository in which changes were made to the R Markdown (analysis/tm.Rmd) and HTML (docs/tm.html) files. If you’ve configured a remote Git repository (see ?wflow_git_remote), click on the hyperlinks in the table below to view the files as they were in that past version.

File Version Author Date Message
Rmd e8b8d76 Dave Tang 2023-11-02 Vignette
html 4011b46 Dave Tang 2023-11-02 Build site.
Rmd 5acae29 Dave Tang 2023-11-02 Text mining using the tm package

The tm package is a framework for text mining applications within R.

library(tm)
Loading required package: NLP
packageVersion("tm")
[1] '0.7.11'

Getting started

The crude corpus:

This data set holds 20 news articles with additional meta information from the Reuters-21578 data set. All documents belong to the topic crude dealing with crude oil.

data(crude)
class(crude)
[1] "VCorpus" "Corpus" 

inspect can be used to display detailed information on a corpus, a term-document matrix, or a text document.

inspect(crude[1:3])
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 3

$`reut-00001.xml`
<<PlainTextDocument>>
Metadata:  15
Content:  chars: 527

$`reut-00002.xml`
<<PlainTextDocument>>
Metadata:  15
Content:  chars: 2634

$`reut-00004.xml`
<<PlainTextDocument>>
Metadata:  15
Content:  chars: 330

Create a Term Document Matrix.

tdm <- TermDocumentMatrix(crude)
tdm
<<TermDocumentMatrix (terms: 1266, documents: 20)>>
Non-/sparse entries: 2255/23065
Sparsity           : 91%
Maximal term length: 17
Weighting          : term frequency (tf)

Convert to matrix.

crude_matrix <- as.matrix(tdm)
dim(crude_matrix)
[1] 1266   20

Check out the matrix. We need to remove the symbols!

crude_matrix[1:6, 1:6]
            Docs
Terms        127 144 191 194 211 236
  ...          0   0   0   0   0   0
  "(it)        0   0   0   0   0   0
  "demand      0   1   0   0   0   0
  "expansion   0   0   0   0   0   0
  "for         0   0   0   0   0   0
  "growth      0   0   0   0   0   0

Sparsity is the number of zeros, i.e., words that are not present in documents.

prop.table(table(crude_matrix == 0))

     FALSE       TRUE 
0.08906003 0.91093997 

Functions that can be used on a TermDocumentMatrix.

methods(class = "TermDocumentMatrix")
 [1] [                     as.DocumentTermMatrix as.TermDocumentMatrix
 [4] c                     dimnames<-            Docs                 
 [7] findAssocs            findMostFreqTerms     inspect              
[10] nDocs                 nTerms                plot                 
[13] print                 t                     Terms                
[16] tm_term_score        
see '?methods' for accessing help and source code

Clean corpus.

clean_corpus <- function(x){
  x |>
    tm_map(removePunctuation) |>
    tm_map(stripWhitespace) |>
    tm_map(content_transformer(function(x) iconv(x, to='UTF-8', sub='byte'))) |>
    tm_map(removeNumbers) |>
    tm_map(removeWords, stopwords("en")) |>
    tm_map(content_transformer(tolower)) |>
    tm_map(removeWords, c("etc","ie", "eg", stopwords("english")))
}

tdm <- TermDocumentMatrix(clean_corpus(crude))

crude_matrix <- as.matrix(tdm)
crude_matrix[1:6, 1:6]
           Docs
Terms       127 144 191 194 211 236
  abdulaziz   0   0   0   0   0   0
  ability     0   2   0   0   0   3
  able        0   0   0   0   0   0
  abroad      0   0   0   0   0   1
  accept      0   0   0   0   0   0
  accord      0   0   0   0   0   0

findFreqTerms finds frequent terms in a document-term or term-document matrix.

findFreqTerms(x = tdm, lowfreq = 10)
 [1] "barrel"     "barrels"    "bpd"        "crude"      "dlrs"      
 [6] "government" "industry"   "kuwait"     "last"       "market"    
[11] "meeting"    "minister"   "mln"        "new"        "official"  
[16] "oil"        "one"        "opec"       "pct"        "price"     
[21] "prices"     "production" "reuter"     "said"       "saudi"     
[26] "sheikh"     "will"       "world"     

Limit matrix to specific words.

inspect(
  x = DocumentTermMatrix(
    x = crude,
    list(dictionary = c("government", "market", "official")
    )
  )
)
<<DocumentTermMatrix (documents: 20, terms: 3)>>
Non-/sparse entries: 15/45
Sparsity           : 75%
Maximal term length: 10
Weighting          : term frequency (tf)
Sample             :
     Terms
Docs  government market official
  144          0      3        0
  236          0      0        5
  237          5      0        0
  242          0      1        1
  246          6      0        0
  248          0      4        1
  273          0      1        4
  349          0      1        2
  352          0      1        1
  704          0      1        0

findAssocs finds associations in a document-term or term-document matrix.

findAssocs(x = tdm, terms = 'government', corlimit = 0.8)
$government
agriculture       early    positive         say       years       since 
       1.00        1.00        1.00        0.91        0.83        0.82 

Simple analysis on the matrix.

Most common words.

head(sort(rowSums(crude_matrix), decreasing = TRUE))
   oil   said prices   opec    mln   last 
    85     73     48     42     31     24 

Correlation.

cor(crude_matrix[,1], crude_matrix[,2])
[1] 0.351976

Clustering.

set.seed(31)
my_cluster <- kmeans(x = t(crude_matrix), centers = 4)
my_cluster$cluster
127 144 191 194 211 236 237 242 246 248 273 349 352 353 368 489 502 543 704 708 
  4   3   4   4   4   3   2   4   1   3   3   4   4   4   4   4   4   4   4   4 

Check headings of cluster 3.

meta(crude, 'heading')[my_cluster$cluster == 3]
$`144`
[1] "OPEC MAY HAVE TO MEET TO FIRM PRICES - ANALYSTS"

$`236`
[1] "KUWAIT SAYS NO PLANS FOR EMERGENCY OPEC TALKS"

$`248`
[1] "SAUDI ARABIA REITERATES COMMITMENT TO OPEC PACT"

$`273`
[1] "SAUDI FEBRUARY CRUDE OUTPUT PUT AT 3.5 MLN BPD"

Vignette

Following the vignette.

The main structure for managing documents in the tm package is a Corpus, which represents a collection of text documents. A corpus is an abstract concept and there can exist several implementations in parallel. The default implementation is the VCorpus, which is short for Volatile Corpus. The PCorpus implements a Permanent Corpus and the documents are physically stored outside of R.

Within the corpus constructor, x must be a Source object which abstracts the input location. tm provides a set of predefined sources, e.g., DirSource, VectorSource, or DataframeSource, which handle a directory, a vector interpreting each component as document, or data frame like structures (like CSV files), respectively.

Below is an example of reading in a plain text file in the directory txt containing Latin (lat) texts by the Roman poet Ovid.

txt <- system.file("texts", "txt", package = "tm")
(ovid <- VCorpus(DirSource(txt, encoding = "UTF-8"), readerControl = list(language = "lat")))
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 5

For simple examples VectorSource is quite useful, as it can create a corpus from character vectors.

docs <- c("This is a text.", "This another one.")
VCorpus(VectorSource(docs))
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 2

The tm package ships with several readers (e.g., readPlain(), readPDF(), and readDOC()). See ?getReaders() for an up-to-date list of available readers.

Create a data frame for creating a corpus.

my_df <- data.frame(
  doc_id = c("doc 1" , "doc 2" , "doc 3" ),
  text = c("this is a sentence", "this is another sentence", "who what how"),
  title = c("title 1" , "title 2" , "title 3" ),
  authors = c("author 1" , "author 2" , "author 3" ),
  topics = c("topic 1" , "topic 2" , "topic 3" ),
  stringsAsFactors = FALSE
)

my_df
  doc_id                     text   title  authors  topics
1  doc 1       this is a sentence title 1 author 1 topic 1
2  doc 2 this is another sentence title 2 author 2 topic 2
3  doc 3             who what how title 3 author 3 topic 3

A data frame source interprets each row of the data frame as a document. The first column must be named doc_id and contain a unique string identifier for each document. The second column must be named text and contain a UTF-8 encoded string representing the document’s content. Optional additional columns are used as document level metadata.

(my_corpus <- Corpus(DataframeSource(my_df)))
<<SimpleCorpus>>
Metadata:  corpus specific: 1, document level (indexed): 3
Content:  documents: 3

Create a TermDocumentMatrix.

my_tdm <- TermDocumentMatrix(my_corpus)
my_tdm
<<TermDocumentMatrix (terms: 6, documents: 3)>>
Non-/sparse entries: 8/10
Sparsity           : 56%
Maximal term length: 8
Weighting          : term frequency (tf)

Check out the matrix.

as.matrix(my_tdm)
          Docs
Terms      doc 1 doc 2 doc 3
  sentence     1     1     0
  this         1     1     0
  another      0     1     0
  how          0     0     1
  what         0     0     1
  who          0     0     1

Transformations

Once we have a corpus we typically want to modify the documents in it, e.g., stemming, stop word removal, etc. In tm, all this functionality is performed via the tm_map() function which applies (maps) a function to all elements of the corpus; this is called a transformation. All transformations work on single text documents and tm_map() just applies them to all documents in a corpus.

reut21578 <- system.file("texts", "crude", package = "tm")
reuters <- VCorpus(DirSource(reut21578, mode = "binary"), readerControl = list(reader = readReut21578XMLasPlain))
reuters
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 20

Remove whitespace.

reuters <- tm_map(reuters, stripWhitespace)

We can use arbitrary character processing functions as transformations as long as the function returns a text document. In this case we use content_transformer() which provides a convenience wrapper to access and set the content of a document. Consequently most text manipulation functions from base R can directly be used with this wrapper. This works for tolower() as used here but also with gsub() which comes quite handy for a broad range of text manipulation tasks.

reuters <- tm_map(reuters, content_transformer(tolower))

Remove stopwords.

reuters <- tm_map(reuters, removeWords, stopwords("english"))

From https://www.geeksforgeeks.org/introduction-to-stemming/:

Stemming is the process of producing morphological variants of a root/base word. Stemming programs are commonly referred to as stemming algorithms or stemmers. A stemming algorithm reduces the words “chocolates”, “chocolatey”, “choco” to the root word, “chocolate” and “retrieval”, “retrieved”, “retrieves” reduce to the stem “retrieve”. Stemming is an important part of the pipelining process in Natural language processing. The input to the stemmer is tokenized words. How do we get these tokenized words? Well, tokenization involves breaking down the document into different words.

This requires the SnowballC package.

library(SnowballC)
tm_map(reuters, stemDocument)
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 20

Often it is of special interest to filter out documents satisfying given properties. For this purpose the function tm_filter is designed. It is possible to write custom filter functions which get applied to each document in the corpus. Alternatively, we can create indices based on selections and subset the corpus with them. E.g., the following statement filters out those documents having an ID equal to “237” and the string “INDONESIA SEEN AT CROSSROADS OVER ECONOMIC CHANGE” as their heading.

idx <- meta(reuters, "id") == '237' & meta(reuters, "heading") == 'INDONESIA SEEN AT CROSSROADS OVER ECONOMIC CHANGE'
reuters[idx]
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 1

Term-Document Matrices

A common approach in text mining is to create a term-document matrix from a corpus. In the tm package the classes TermDocumentMatrix and DocumentTermMatrix (depending on whether you want terms as rows and documents as columns, or vice versa) employ sparse matrices for corpora. Inspecting a term-document matrix displays a sample, whereas as.matrix() yields the full matrix in dense format (which can be very memory consuming for large matrices).

dtm <- DocumentTermMatrix(reuters)
inspect(dtm)
<<DocumentTermMatrix (documents: 20, terms: 1183)>>
Non-/sparse entries: 1908/21752
Sparsity           : 92%
Maximal term length: 17
Weighting          : term frequency (tf)
Sample             :
     Terms
Docs  crude dlrs last mln oil opec prices reuter said saudi
  144     0    0    1   4  11   10      3      1    9     0
  236     1    2    4   4   7    6      2      1    6     0
  237     0    1    3   1   3    1      0      1    0     0
  242     0    0    0   0   3    2      1      1    3     1
  246     0    0    2   0   4    1      0      1    4     0
  248     0    3    1   3   9    6      7      1    5     5
  273     5    2    7   9   5    5      4      1    5     7
  489     0    1    0   2   4    0      2      1    2     0
  502     0    1    0   2   4    0      2      1    2     0
  704     0    0    0   0   3    0      2      1    3     0

Find terms that occur at least five times using the findFreqTerms() function.

findFreqTerms(dtm, 5)
 [1] "15.8"          "abdul-aziz"    "ability"       "accord"       
 [5] "agency"        "agreement"     "ali"           "also"         
 [9] "analysts"      "arab"          "arabia"        "barrel."      
[13] "barrels"       "billion"       "bpd"           "budget"       
[17] "company"       "crude"         "daily"         "demand"       
[21] "dlrs"          "economic"      "emergency"     "energy"       
[25] "exchange"      "expected"      "exports"       "futures"      
[29] "government"    "group"         "gulf"          "help"         
[33] "hold"          "industry"      "international" "january"      
[37] "kuwait"        "last"          "market"        "may"          
[41] "meeting"       "minister"      "mln"           "month"        
[45] "nazer"         "new"           "now"           "nymex"        
[49] "official"      "oil"           "one"           "opec"         
[53] "output"        "pct"           "petroleum"     "plans"        
[57] "posted"        "present"       "price"         "prices"       
[61] "prices,"       "prices."       "production"    "quota"        
[65] "quoted"        "recent"        "report"        "research"     
[69] "reserve"       "reuter"        "said"          "said."        
[73] "saudi"         "sell"          "sheikh"        "sources"      
[77] "study"         "traders"       "u.s."          "united"       
[81] "west"          "will"          "world"        

Find associations (i.e., terms which correlate) with at least 0.8 correlation for the term “opec” using the findAssocs() function.

findAssocs(dtm, "opec", 0.8)
$opec
  meeting emergency       oil      15.8  analysts    buyers      said   ability 
     0.88      0.87      0.87      0.85      0.85      0.83      0.82      0.80 

Term-document matrices tend to get very big already for normal sized data sets. Therefore we provide a method to remove sparse terms, i.e., terms occurring only in very few documents. Normally, this reduces the matrix dramatically without losing significant relations inherent to the matrix:

inspect(removeSparseTerms(dtm, 0.4))
<<DocumentTermMatrix (documents: 20, terms: 3)>>
Non-/sparse entries: 58/2
Sparsity           : 3%
Maximal term length: 6
Weighting          : term frequency (tf)
Sample             :
     Terms
Docs  oil reuter said
  127   5      1    1
  144  11      1    9
  236   7      1    6
  242   3      1    3
  246   4      1    4
  248   9      1    5
  273   5      1    5
  352   5      1    1
  489   4      1    2
  502   4      1    2

A dictionary is a (multi-)set of strings. It is often used to denote relevant terms in text mining. We represent a dictionary with a character vector which may be passed to the DocumentTermMatrix() constructor as a control argument. Then the created matrix is tabulated against the dictionary, i.e., only terms from the dictionary appear in the matrix. This allows to restrict the dimension of the matrix a priori and to focus on specific terms for distinct text mining contexts.

inspect(DocumentTermMatrix(reuters, list(dictionary = c("prices", "crude", "oil"))))
<<DocumentTermMatrix (documents: 20, terms: 3)>>
Non-/sparse entries: 41/19
Sparsity           : 32%
Maximal term length: 6
Weighting          : term frequency (tf)
Sample             :
     Terms
Docs  crude oil prices
  127     2   5      3
  144     0  11      3
  236     1   7      2
  248     0   9      7
  273     5   5      4
  352     0   5      4
  353     2   4      1
  489     0   4      2
  502     0   4      2
  543     2   2      2

sessionInfo()
R version 4.3.2 (2023-10-31)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 22.04.3 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.20.so;  LAPACK version 3.10.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

time zone: Etc/UTC
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] SnowballC_0.7.1 tm_0.7-11       NLP_0.2-1       workflowr_1.7.1

loaded via a namespace (and not attached):
 [1] jsonlite_1.8.7    compiler_4.3.2    promises_1.2.1    Rcpp_1.0.11      
 [5] slam_0.1-50       xml2_1.3.5        stringr_1.5.0     git2r_0.32.0     
 [9] parallel_4.3.2    callr_3.7.3       later_1.3.1       jquerylib_0.1.4  
[13] yaml_2.3.7        fastmap_1.1.1     R6_2.5.1          knitr_1.45       
[17] tibble_3.2.1      rprojroot_2.0.3   bslib_0.5.1       pillar_1.9.0     
[21] rlang_1.1.1       utf8_1.2.4        cachem_1.0.8      stringi_1.7.12   
[25] httpuv_1.6.12     xfun_0.40         getPass_0.2-2     fs_1.6.3         
[29] sass_0.4.7        cli_3.6.1         magrittr_2.0.3    ps_1.7.5         
[33] digest_0.6.33     processx_3.8.2    rstudioapi_0.15.0 lifecycle_1.0.3  
[37] vctrs_0.6.4       evaluate_0.22     glue_1.6.2        whisker_0.4.1    
[41] fansi_1.0.5       rmarkdown_2.25    httr_1.4.7        tools_4.3.2      
[45] pkgconfig_2.0.3   htmltools_0.5.6.1