Susan Li bio photo

Susan Li

Data Analyst works in an AI company.

Twitter LinkedIn Github

My Favorite Blogs

via GIPHY

Dickens wrote fourteen and a half novels. I will start from analyzing five of them - “A Tale of Two Cities”, “Great Expectations”, “A Christmas Carol in Prose; Being a Ghost Story of Christmas”, “Oliver Twist” and “Hard Times”.

Project Gutenberg offers over 53,000 free books. I will download Dickens’ novels in UTF-8 encoded texts from there using gutenbergr package developed by David Robinson. Besides, I will be using the following packages for this project.

library(dplyr)
library(tm.plugin.webmining)
library(purrr)
library(tidytext)
library(gutenbergr)
library(ggplot2)
dickens <- gutenberg_download(c(98, 1400, 46, 730, 786))

Download Dickens’ five novels by Project Gutenberg ID numbers.

tidy_dickens <- dickens %>%
  unnest_tokens(word, text) %>%
  anti_join(stop_words)

The unnest_tokens package is used to split each row so that there is one token (word) in each row of the new data frame (tidy_dickens). Then remove stop words with an anti_join function.

tidy_dickens %>%
  count(word, sort = TRUE)
### A tibble: 19,634 × 2
##     word     n
##    <chr> <int>
##1    time  1218
##2    hand   918
##3   night   835
##4  looked   814
##5    head   813
##6  oliver   766
##7    dear   751
##8     joe   718
##9    miss   702
##10    sir   697
### ... with 19,624 more rows

After removing the stop words, here is a list of words starts from the most frequent.

tidy_dickens %>%
  count(word, sort = TRUE) %>%
  filter(n > 600) %>%
  mutate(word = reorder(word, n)) %>%
  ggplot(aes(word, n)) +
  geom_col() +
  xlab(NULL) +
  coord_flip() + ggtitle("The Most Common Words in Charles Dickens' Novels")

dickens-1

Sentiment in Dickens’ Five Novels

tidytext package contains several sentiment lexicons, I am using “bing” for the following tasks.

bing_word_counts <- tidy_dickens %>%
  inner_join(get_sentiments("bing")) %>%
  count(word, sentiment, sort = TRUE) %>%
  ungroup()
bing_word_counts
### A tibble: 3,145 × 3
##     word sentiment     n
##    <chr>     <chr> <int>
##1    miss  negative   702
##2    poor  negative   350
##3    dark  negative   299
##4    hard  negative   223
##5    dead  negative   218
##6  strong  positive   203
##7    love  positive   202
##8    fell  negative   198
##9   death  negative   194
##10   cold  negative   192
### ... with 3,135 more rows

Here I got the sentiment categories of Dickens’ words.

bing_word_counts %>%
  group_by(sentiment) %>%
  top_n(10) %>%
  ungroup() %>%
  mutate(word = reorder(word, n)) %>%
  ggplot(aes(word, n, fill = sentiment)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~sentiment, scales = "free_y") +
  labs(y = "Words Contribute to sentiment",
       x = NULL) +
  coord_flip()

dickens-2

The word “miss” is the most frequent negative word here, but it is used to describe unmarried women in Dickens’ works. In particualr, Miss Havisham is a significant character in the Charles Dickens novel “Great Expectations”. Dickens describes her as looking like “the witch of the place”. In this case, probably “miss”” should be a negative word.

via GIPHY

Oh poor Pip!

library(wordcloud)
tidy_dickens %>%
  anti_join(stop_words) %>%
  count(word) %>%
  with(wordcloud(word, n, max.words = 100))

dickens-3

Word cloud is a good idea to identify trends and patterns that would otherwise be unclear or difficult to see in a tabular format.

library(reshape2)
tidy_dickens %>%
  inner_join(get_sentiments("bing")) %>%
  count(word, sentiment, sort = TRUE) %>%
  acast(word ~ sentiment, value.var = "n", fill = 0) %>%
  comparison.cloud(colors = c("#F8766D", "#00BFC4"),
                   max.words = 100)

dickens-4

And compare most frequent positive and negative words in word cloud.

Relationships between words

dickens_bigrams <- dickens %>%
  unnest_tokens(bigram, text, token = "ngrams", n = 2)
dickens_bigrams
### A tibble: 616,994 × 2
##   gutenberg_id          bigram
##          <int>           <chr>
##1            46     a christmas
##2            46 christmas carol
##3            46        carol in
##4            46        in prose
##5            46     prose being
##6            46         being a
##7            46         a ghost
##8            46     ghost story
##9            46        story of
##10           46    of christmas
### ... with 616,984 more rows

Each token now represents a bigram (two words paired). if one of the words in the bigram is a stop word, this word will be removed. After filtering out stop words, what are the most frequent bigrams?

library(tidyr)
bigrams_separated <- dickens_bigrams %>%
  separate(bigram, c("word1", "word2"), sep = " ")
bigrams_filtered <- bigrams_separated %>%
  filter(!word1 %in% stop_words$word) %>%
  filter(!word2 %in% stop_words$word)
bigram_counts <- bigrams_filtered %>% 
  count(word1, word2, sort = TRUE)
bigram_counts
##Source: local data frame [41,584 x 3]
##Groups: word1 [10,206]

##    word1      word2     n
##    <chr>      <chr> <int>
##1    miss   havisham   236
##2    miss      pross   144
##3  madame    defarge   113
##4    dear        boy    77
##5      ha         ha    77
##6  doctor    manette    75
##7    miss havisham's    74
##8  oliver      twist    55
##9     sir    replied    54
##10   wine       shop    52
### ... with 41,574 more rows

Names are the most common paired words in Dickens’ novels.

bigrams_united <- bigrams_filtered %>%
  unite(bigram, word1, word2, sep = " ")
bigrams_united
### A tibble: 51,379 × 2
##   gutenberg_id            bigram
##*         <int>             <chr>
##1            46   christmas carol
##2            46       ghost story
##3            46   charles dickens
##4            46   dickens preface
##5            46 houses pleasantly
##6            46   faithful friend
##7            46     december 1843
##8            46     1843 contents
##9            46    contents stave
##10           46    marley's ghost
### ... with 51,369 more rows

Before visualizing it, I have to make these bigrams back to be united.

bigram_tf_idf <- bigrams_united %>%
  count(bigram)
bigram_tf_idf <- bigram_tf_idf %>% filter(n>30)
ggplot(aes(x = reorder(bigram, n), y=n), data=bigram_tf_idf) + geom_bar(stat = 'identity') + ggtitle("The Most Common Bigrams in Dickens' novels") + coord_flip()

dickens-5

Yes, the most frequent bigrams in Dickens’ works are names. I also notice some pairings of a common verb such as “wine shop” from “A Tale of Two Cities” and “oliver twist”.

At last, visualizing a network of bigrams of Dickens’ five novels.

library(igraph)
bigram_graph <- bigram_counts %>%
  filter(n > 20) %>%
  graph_from_data_frame()
bigram_graph
##IGRAPH DN-- 61 37 -- 
##+ attr: name (v/c), n (e/n)
##+ edges (vertex names):
## [1] miss    ->havisham   miss    ->pross      madame  ->defarge   
## [4] dear    ->boy        ha      ->ha         doctor  ->manette   
## [7] miss    ->havisham's oliver  ->twist      sir     ->replied   
##[10] wine    ->shop       charles ->darnay     replied ->oliver    
##[13] master  ->bates      miss    ->manette    saint   ->antoine   
##[16] james   ->harthouse  sir     ->returned   charley ->bates     
##[19] low     ->voice      stephen ->blackpool  sydney  ->carton    
##[22] god     ->bless      monsieur->defarge    public  ->house     
##+ ... omitted several edges
library(ggraph)
set.seed(2017)
ggraph(bigram_graph, layout = "fr") +
  geom_edge_link() +
  geom_node_point(color = "darkslategray4", size = 3) +
  geom_node_text(aes(label = name), vjust = 1.8) + ggtitle("Common Bigrams in Dickens' five Novels")

dickens-6

Wow, that was so much fun! I don’t want to finish as yet. I want to look into one of these five novels - “A Tale of Two Cities”.

This time I will download the plain text file for “A Tale of Two Cities” only, leave out the Project Gutenberg header and footer information, then concatenate these lines into paragraphs as following:

library(readr)
library(stringr)
raw_tale <- read_lines("ta98-0.txt", skip = 30, n_max = 15500)
tale <- character()
for (i in seq_along(raw_tale)) {
        if (i%%10 == 1) tale[ceiling(i/10)] <- str_c(raw_tale[i], 
                                                     raw_tale[i+1],
                                                     raw_tale[i+2],
                                                     raw_tale[i+3],
                                                     raw_tale[i+4],
                                                     raw_tale[i+5],
                                                     raw_tale[i+6],
                                                     raw_tale[i+7],
                                                     raw_tale[i+8],
                                                     raw_tale[i+9], sep = " ")
}
tale[9:10]
##[1] "I. The Period   It was the best of times, it was the worst of times, it ##was the age of wisdom, it was the age of foolishness, it was the epoch of ##belief, it was the epoch of incredulity, it was the season of Light,"                                                                                    ##[2] "it was the season of Darkness, it was the spring of hope, it was the ##winter of despair, we had everything before us, we had nothing before us, we ##were all going direct to Heaven, we were all going direct the other way-- in ##short, the period was so far like the present period, that some of its ##noisiest authorities insisted on its being received, for good or for evil, ##in the superlative degree of comparison only."

Sentiment in “A Tale of Two Cities”

Apply NRC sentiment dictionary to this novel.

library(syuzhet)
tale_nrc <- cbind(linenumber = seq_along(tale), get_nrc_sentiment(tale))

Create a data frame combine the line number of the book with the sentiment score, then extract positive and negative scores for visualization.

tale_nrc$negative <- -tale_nrc$negative
pos_neg <- tale_nrc %>% select(linenumber, positive, negative) %>% 
        melt(id = "linenumber")
names(pos_neg) <- c("linenumber", "sentiment", "value")
library(ggthemes)
ggplot(data = pos_neg, aes(x = linenumber, y = value, fill = sentiment)) +
        geom_bar(stat = 'identity', position = position_dodge()) + theme_minimal() +
        ylab("Sentiment") + 
        ggtitle("Positive and Negative Sentiment in A Tale of Two Cities") +
  scale_color_manual(values = c("orange", "blue")) +
  scale_fill_manual(values = c("orange", "blue"))

dickens-7

Seems the positive scores and the negative scores are almost equal overall, it does make sense given the content of the novel.

emotions <- tale_nrc %>% select(linenumber, anger, anticipation, 
                                      disgust, fear, joy, sadness, surprise, 
                                      trust) %>% 
        melt(id = "linenumber")
names(emotions) <- c("linenumber", "sentiment", "value")
emotions_group <- group_by(emotions, sentiment)
by_emotions <- summarise(emotions_group, 
                         values=sum(value))
ggplot(aes(reorder(x=sentiment, values), y=values, fill=sentiment), data = by_emotions) +
  geom_bar(stat = 'identity') + ggtitle('Sentiment in A Tale of Two Cities') +
  coord_flip() + theme(legend.position="none")

dickens-8

The End

Again, that was so much fun, my post just touched a bit of it on text mining. There are many more things to do such as comparing text across different novelists, save it to the next time.

Source code that created this post can be found here. I am happy to hear any feedback and questions.

References:

The gutenbergr package

Text Mining With R