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.
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")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)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.
#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.
#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 palabrasEl 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.
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
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()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.
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
# 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()# 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.
# 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()# 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.
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.