1 Introducción

Spice and Wolf (狼と香辛料, Ōkami to Kōshinryō) es una serie de novelas ligeras escritas por Isuna Hasekura. La publicación de las novelas comenzaron el 2006 y hasta el día de hoy lleva 22 novelas escritas siendo una de las novelas más leídas en japón.

Aprovechando el trabajo que realicé ayudando en la traducción de la novela al español, mi fanatismo por la serie y la necesidad de realizar un análisis para el ramo de PNL del magister en Data Science de la Universidad del Desarrollo realizaré el análisis de sentimientos del mundo de las novelas de Spice and Wolf.

Para este informe se siguió el trabajo de Xavier en sus análisis de Star Wars y Lord of the Rings.

2 Carga de librerías y archivos

Para la obtención del texto de las novelas se realizó la transcripción de las novelas ligeras a archivos de texto. Junto a estos archivos de texto se realizará la carga de los lexicon para realizar el análisis de sentimientos. Además para este trabajo se necesitarán herramientas de text mining, manipulación de dataframes, entre otros, especificados a continuación:

#Se cargan las librerías
library(tidyverse) #Manipulación de datos
library(tm) #Text mining
library(wordcloud) #Generador de nube de palabras
library(wordcloud2) #Generador de nube de palabras
library(tidytext) #Text mining y procesado de palabras
library(reshape2) #Modificación a dataframes
library(radarchart) #Adición a ggplot2
library(RWeka) #Data Mining
library(knitr) #Generación de markdowns
library(readtext) #Para la lectura de los txt

#Se cargan los archivos
FILEDIR = "data_holo/PDF/"
filenames <- list.files(FILEDIR)
filenames <- gsub(".txt$", "", filenames)
txts <- readtext(FILEDIR)

#Se cargan los lexicons para el análisis de sentimiento
bing <- read_csv("data/lexi/Bing.csv")
nrc <- read_csv("data/lexi/NRC.csv")
afinn <- read_csv("data/lexi/Afinn.csv")

3 Preprocesado de las novelas

Antes de realizar cualquier análisis es necesario ordenar las columnas según su orden de publicación. Además se debe limpiar el texto para poder utilizarlo. La limpieza consistirá en eliminar carácteres extraños, tabulaciones, espacios, saltos de línea, números, stopwords y puntuaciones.

#Se limpian los archivos, para ello se crea el orden de las novelas para crear el factor
order_levels <- c("Spice and Wolf  Vol1","Spice and Wolf  Vol2","Spice and Wolf  Vol3",
                  "Spice and Wolf  Vol4","Spice and Wolf  Vol5","Spice and Wolf  Vol6",
                  "Spice and Wolf  Vol7","Spice and Wolf  Vol8","Spice and Wolf  Vol9",
                  "Spice and Wolf  Vol10","Spice and Wolf  Vol11","Spice and Wolf  Vol12",
                  "Spice and Wolf  Vol13","Spice and Wolf  Vol14","Spice and Wolf  Vol15",
                  "Spice and Wolf  Vol16","Spice and Wolf  Vol17","Spice and Wolf  Vol18",
                  "Spice and Wolf  Vol19","Spice and Wolf  Vol20","Spice and Wolf  Vol21",
                  "Spice and Wolf s2 Vol1","Spice and Wolf s2 Vol2"
                  )
#Se cambia el tipo de columna con los nombres a factor
txts$doc_id <- factor(txts$doc_id, levels = order_levels)


#Se crea una función para eliminar el caracter 's expresado en el código de lectura como ’s
tryfunc <- function(x) gsub("’s", "", x)
#Limpieza del texto, se remueven todos los carácteres que no se quieren utilizar
cleanCorpus <- function(corpus){
  s.cor <- Corpus(VectorSource(corpus))
  corpus.tmp <- tm_map(s.cor, function(x) gsub("\\t", "  ", x)) #Eliminar tabulaciones
  corpus.tmp <- tm_map(corpus.tmp, function(x) gsub("\\n", "  ", x)) #Eliminar saltos de línea
  corpus.tmp <- tm_map(corpus.tmp, function(x) gsub("\\—", "  ", x)) #Eliminar doble -- unido
  corpus.tmp <- tm_map(corpus.tmp, removePunctuation) #Remover puntuaciones
  corpus.tmp <- tm_map(corpus.tmp, stripWhitespace) #Remover espacios
  corpus.tmp <- tm_map(corpus.tmp, content_transformer(tolower)) #Dejar en minúscula
  v_stopwords <- c(stopwords("english"), c("thats","weve","hes","theres","ive","im",
                                           "will","can","cant","dont","youve","us",
                                           "youre","youll","theyre","whats","didnt",
                                           "chapter","p","g","e")) 
  corpus.tmp <- tm_map(corpus.tmp, removeWords, v_stopwords) #Eliminar stopwords y otras palabras
  corpus.tmp <- tm_map(corpus.tmp, removeNumbers) #Eliminar números
  corpus.tmp <- tm_map(corpus.tmp, PlainTextDocument) #Dejar texto plano
  return(corpus.tmp[[1]][1])} #Retorna el array dentro del corpus creado

#Se crean dos bucles. El primero elimina el caracter 's
for(i in 1:length(txts$doc_id)){
  txts[i,2] <- tryfunc(txts[i,2])
}

#Luego se intercambia el típo de código para que aparezcan las tabulaciones y saltos
#como \t y \n
txts$text<- iconv(txts$text, 'utf-8', 'ascii', sub='')

#Se aplica la función cleanCorpus
for(i in 1:length(txts$doc_id)){
  txts[i,2] <- cleanCorpus(txts[i,2])
}

#Se ordena el dataframe según el factor creado
txts <- txts %>% arrange(doc_id)

4 Analisis de los datos

Una forma visual de comenzar el análisis es realizar una nube de palabras. Por problemas con la librería se realizará una captura del wordcloud y se publicará como imagen.

4.1 Nube de palabras según su frecuencia

#Se crea otra función CleanCorpus para poder ser utilizada en la siguiente función
cleanCorpus2 <- function(corpus){
  corpus.tmp <- tm_map(corpus, removePunctuation)
  corpus.tmp <- tm_map(corpus.tmp, stripWhitespace)
  corpus.tmp <- tm_map(corpus.tmp, content_transformer(tolower))
  v_stopwords <- c(stopwords("english"), c("thats","weve","hes","theres","ive","im",
                                           "will","can","cant","dont","youve","us",
                                           "youre","youll","theyre","whats","didnt"))
  corpus.tmp <- tm_map(corpus.tmp, removeWords, v_stopwords)
  return(corpus.tmp)
  
}

# Se crea una función para realizar el conteo de palabras
frequentTerms <- function(text){
  #La funcion Corpus del paquete tm crea  y computa el corpus
  #Mientras que la función VectorSource interpreta cada elemento de 
  #text como un documento
  s.cor <- Corpus(VectorSource(text))
  #Se aplica la primera función creada cleanCorpus2
  s.cor.cl <- cleanCorpus2(s.cor)
  #Del paquete tm transforma el documento a una matriz
  s.tdm <- TermDocumentMatrix(s.cor.cl)
  #Remueve elementos que aparecen en pequeñas cantidades
  s.tdm <- removeSparseTerms(s.tdm, 0.999)
  #Transforma los datos a una matriz de R
  m <- as.matrix(s.tdm)
  #Se suma por fila para obtener la frecuencia
  #Como está ordenado como matriz se ordena según la frecuencia más alta
  word_freqs <- sort(rowSums(m), decreasing=TRUE)
  #Se crea un dataframe con estas frecuencias
  dm <- data.frame(word=names(word_freqs), freq=word_freqs)
  return(dm)
  
}

#wordcloud2 tiene problemas al crear las nubes cuando palabras destacan mucho por sobre otras, por lo cual se le asignó un valor más adecuado.
jj <- frequentTerms(txts$text)
jj[1,2] = 7000
jj[2,2] = 6000

hw = wordcloud2(jj, size=3, figPath ="../Imagen4.png")

Se puede observar que las palabras que más aparecen es Holo y Lawrence, protagonistas de la serie.

A continuación se realizará el wordcloud de las palabras separadas según el lexicon genérico de bing, de Bing Liu y colaboradores que nos ayudará a separar palabras según negativas y positivas.

4.2 Nube de palabras según su sentimiento

#Tokeniza el texto de las novelas
tokens <- txts %>%  
  mutate(text=as.character(txts$text)) %>%
  unnest_tokens(word, text)

tokens %>%
  inner_join(get_sentiments("bing")) %>% #Se agrega si son palabras positivas o negativas
  count(word, sentiment, sort=TRUE) %>% #Se realiza el conteo de las palabras
  acast(word ~ sentiment, value.var="n", fill=0) %>% #Separa las columnas según es positivo o negativo
  comparison.cloud(colors=c("#3399FF", "#CC6600"), max.words=300, title.size = 2, title.colors = c("#3399FF", "#CC6600")) #Se crea la nube de palabras

4.3 Barplot según la frecuencia del sentimiento

El siguiente análisis utiliza el lexicon nrc, de Saif Mohammad y Peter Turney, el cual no solo contiene la denominación de las palabras como positivo y negativo, contiene sentimientos de verdad, anticipación, sorpresa, enojo, entre otros.

# Sentiments and frequency associated with each word  
sentiments <- tokens %>% 
  inner_join(nrc, "word") %>%
  count(word, sentiment, sort=TRUE) 

# Frequency of each sentiment
ggplot(data=sentiments, aes(x=reorder(sentiment, -n, sum), y=n)) + 
  geom_bar(stat="identity", aes(fill=sentiment), show.legend=FALSE) +
  scale_fill_brewer(palette="Paired") +
  labs(x="Sentiment", y="Frequency") +
  theme_bw() 

Se puede observar que el sentimiento que predomina es el positivo sobre el negativo. Además de sentimientos como verdad y anticipación, debido a que es una novela de una pareja de comerciantes, es más que esperable que aparezcan.

4.4 Palabras más frecuentes según el sentimiento

A continuación se realizará el análisis de palabras más frecuentes según el sentimiento. Esto se realizará considerando todas las novelas en su conjunto.

sentiments %>%
  group_by(sentiment) %>%
  arrange(desc(n)) %>%
  slice(1:10) %>%
  ggplot(aes(x=reorder(word, n), y=n)) +
  geom_col(aes(fill=sentiment), show.legend=FALSE)  +
  scale_fill_manual(values=replicate(10,"#CC6600")) +
  facet_wrap(~sentiment, scales="free_y") +
  labs(y="Frequency", x="Terms") +
  coord_flip() +
  theme_bw() 

Se puede observar que palabras como money, ill, church, lie, god son palabras muy esperadas para una novela ambientada en el tiempo medieval.

Otro análisis a realizar será el de sentimientos pero por novela. Debido al tamaño de los gráficos separará desde la novela 1 a la 10 y de la 11 a la 23

4.4.1 Novelas 1-12

tokens %>%
  filter(doc_id %in% order_levels[1:12]) %>%
  inner_join(nrc, "word") %>%
  count(doc_id, sentiment, sort=TRUE) %>%
  ggplot(aes(x=sentiment, y=n)) +
  geom_col(aes(fill=sentiment), show.legend=FALSE) +
  scale_fill_manual(values=replicate(10,"#CC6600")) +
  facet_wrap(~doc_id, scales="free_y") +
  labs(x="Sentimiento", y="Frecuencia") +
  coord_flip() +
  theme_bw()

4.4.2 Novelas 13-23

tokens %>%
  filter(doc_id %in% order_levels[13:23]) %>%
  inner_join(nrc, "word") %>%
  count(doc_id, sentiment, sort=TRUE) %>%
  ggplot(aes(x=sentiment, y=n)) +
  geom_col(aes(fill=sentiment), show.legend=FALSE) +
  scale_fill_manual(values=replicate(10,"#CC6600")) +
  facet_wrap(~doc_id, scales="free_y") +
  labs(x="Sentimiento", y="Frecuencia") +
  coord_flip() +
  theme_bw()

Se puede observar comportamientos muy parecidos entre las novelas. Exceptuando por el volumen 16 donde el sentimiento negativo sobrepasa el positivo, lo cual si se sigue la lectura de este volumen es esperable al encontrarse en un ambiente de guerras entre ciudades.

4.5 Análisis de importancia de palabras según el Volumen

Finalmente se realizará el análisis de las palabras más repetidas por volumen. Nuevamente se separará entre los volumen 1-10 y 11-23

4.5.1 Novelas 1-12

# Most relevant words for each character
mystopwords <- data_frame(word=c(stopwords("english"), 
                                 c("thats","weve","hes","theres","ive","im",
                                   "will","can","cant","dont","youve","us",
                                   "youre","youll","theyre","whats","didnt")))

# Tokens without stopwords
top.chars.tokens <- txts %>%
  arrange(doc_id) %>%
  slice_head(n = 12) %>%
  mutate(text=as.character(txts[1:12,1:2]$text)) %>%
  unnest_tokens(word, text) %>%
  anti_join(mystopwords, by="word")

# Most relevant words for each novel
top.chars.tokens %>%
  count(doc_id, word) %>%
  group_by(doc_id) %>% 
  arrange(desc(n)) %>%
  slice(1:10) %>%
  ungroup() %>%
  mutate(word2=factor(paste(word, doc_id, sep="__"), 
                      levels=rev(paste(word, doc_id, sep="__"))))%>%
  ggplot(aes(x=word2, y=n)) +
  geom_col(aes(fill=doc_id), show.legend=FALSE) +
  facet_wrap(~doc_id, scales="free_y") +
  theme(axis.text.x=element_text(angle=45, hjust=1)) +
  labs(y="tf–idf", x="Sentiment") +
  scale_x_discrete(labels=function(x) gsub("__.+$", "", x)) +
  coord_flip() +
  theme_bw()

4.5.2 Novelas 13-23

# Most relevant words for each character
mystopwords <- data_frame(word=c(stopwords("english"), 
                                 c("thats","weve","hes","theres","ive","im",
                                   "will","can","cant","dont","youve","us",
                                   "youre","youll","theyre","whats","didnt",
                                   "klass","aryes")))

# Tokens without stopwords
top.chars.tokens <- txts %>%
  arrange(doc_id) %>%
  slice_tail(n = 11) %>%
  mutate(text=as.character(txts[13:23,1:2]$text)) %>%
  unnest_tokens(word, text) %>%
  anti_join(mystopwords, by="word")

# Most relevant words for each novel
top.chars.tokens %>%
  count(doc_id, word) %>%
  group_by(doc_id) %>% 
  arrange(desc(n)) %>%
  slice(1:10) %>%
  ungroup() %>%
  mutate(word2=factor(paste(word, doc_id, sep="__"), 
                      levels=rev(paste(word, doc_id, sep="__"))))%>%
  ggplot(aes(x=word2, y=n)) +
  geom_col(aes(fill=doc_id), show.legend=FALSE) +
  facet_wrap(~doc_id, scales="free_y") +
  theme(axis.text.x=element_text(angle=45, hjust=1)) +
  labs(y="tf–idf", x="Sentiment") +
  scale_x_discrete(labels=function(x) gsub("__.+$", "", x)) +
  coord_flip() +
  theme_bw()

Es de esperar que las palabras más repetidas sean Lawrence y Holo al ser los protagonistas de la serie o personajes muy importantes como Fleur en la novela 3 o Myuri y Col en las dos últimas novelas donde toman el protagonismo de las novelas. El problema es que estas eclipsan la aparición de otras palabras quizás más relevantes, dando importancia solo por su frecuencia, es por esto que se tomará otro enfoque.

Para obtener la relevancia de cada palabra se utilizará la función bind_tf_idf (term frequency -inverse document frecuency). Esta función encuentra las palabras más importantes del documento disminuyendo el peso de las palabras más frecuentes e incrementando el peso de aquellas que no.

4.5.3 Novelas 1-12

# Most relevant words for each character
mystopwords <- data_frame(word=c(stopwords("english"), 
                                 c("thats","weve","hes","theres","ive","im",
                                   "will","can","cant","dont","youve","us",
                                   "youre","youll","theyre","whats","didnt")))

# Tokens without stopwords
top.chars.tokens <- txts %>%
  arrange(doc_id) %>%
  slice_head(n = 12) %>%
  mutate(text=as.character(txts[1:12,1:2]$text)) %>%
  unnest_tokens(word, text) %>%
  anti_join(mystopwords, by="word")

# Most relevant words for each novel
top.chars.tokens %>%
  count(doc_id, word) %>%
  bind_tf_idf(word, doc_id, n) %>%
  group_by(doc_id) %>% 
  arrange(desc(tf_idf)) %>%
  slice(1:10) %>%
  ungroup() %>%
  mutate(word2=factor(paste(word, doc_id, sep="__"), 
                      levels=rev(paste(word, doc_id, sep="__"))))%>%
  ggplot(aes(x=word2, y=tf_idf)) +
  geom_col(aes(fill=doc_id), show.legend=FALSE) +
  facet_wrap(~doc_id, scales="free_y") +
  theme(axis.text.x=element_text(angle=45, hjust=1)) +
  labs(y="tf–idf", x="Sentiment") +
  scale_x_discrete(labels=function(x) gsub("__.+$", "", x)) +
  coord_flip() +
  theme_bw()

4.5.4 Novelas 13-23

# Most relevant words for each character
mystopwords <- data_frame(word=c(stopwords("english"), 
                                 c("thats","weve","hes","theres","ive","im",
                                   "will","can","cant","dont","youve","us",
                                   "youre","youll","theyre","whats","didnt",
                                   "klass","aryes")))

# Tokens without stopwords
top.chars.tokens <- txts %>%
  arrange(doc_id) %>%
  slice_tail(n = 11) %>%
  mutate(text=as.character(txts[13:23,1:2]$text)) %>%
  unnest_tokens(word, text) %>%
  anti_join(mystopwords, by="word")

# Most relevant words for each novel
top.chars.tokens %>%
  count(doc_id, word) %>%
  bind_tf_idf(word, doc_id, n) %>%
  group_by(doc_id) %>% 
  arrange(desc(tf_idf)) %>%
  slice(1:10) %>%
  ungroup() %>%
  mutate(word2=factor(paste(word, doc_id, sep="__"), 
                      levels=rev(paste(word, doc_id, sep="__"))))%>%
  ggplot(aes(x=word2, y=tf_idf)) +
  geom_col(aes(fill=doc_id), show.legend=FALSE) +
  facet_wrap(~doc_id, scales="free_y") +
  theme(axis.text.x=element_text(angle=45, hjust=1)) +
  labs(y="tf–idf", x="Sentiment") +
  scale_x_discrete(labels=function(x) gsub("__.+$", "", x)) +
  coord_flip() +
  theme_bw()

Al darle peso a otras palabras nos encontramos con los personajes secundarios que generan la trama de cada volumen, Fleur y Milton en el Vol11, Fran en el Vol12, Hilde en el volumen 16, Luward en el volumen 15 entre otros. Además se puede obtener una idea de las novelas según algunas palabras que aparecieron, como shepherd (pastor) en el Vol2, pyrite (pirita) en el Vol3, boat (bote) en el Vol6, entre otros, que son parte de las temáticas más importantes de cada volumen respectivamente.

5 Conclusión

El análisis de sentimiento nos permitió comprender un poco más la forma de escritura del autor, generalmente utilizando más palabras positivas que negativas, pero siguiendo una distribución parecida entre los distintos volúmenes; dando mucha importancia al repetir el nombre de los personajes principales y secundarios, de dar importancia a palabras que tienen relación a la temática del volumen.