#La analítica de texto extrae el significado del lenguaje humano y tiene el poder de ofrecer a las empresas información sobre grandes cantidades de datos, como las opiniones de los clientes; ideas que se pueden utilizar para impulsar las decisiones comerciales.
#Para el modelo de aprendizaje automático, construiré un clasificador de sentimientos utilizando árboles de clasificación y, con suerte, modelos más complejos, como un bosque aleatorio. El clasificador de sentimientos capacitado se puede utilizar en otras revisiones en línea que no necesariamente proporcionan una calificación de estrellas. No tengo la intención de utilizar un léxico de sentimientos, en cambio, voy a mirar las calificaciones de revisión (estrellas) para asignar sentimientos.
#Objetivo futuro: Usar el modelo de opinión capacitado para predecir la opinión de un conjunto de datos diferente, como los datos de sugerencias (que son revisiones cortas y consejos que no tienen una calificación de estrellas)
4.1. Eliminar opiniones que no están en inglés 4.2. Crear variable de salida (sentimiento) a partir de la calificación de estrellas 4.3. Creación de variables de entrada: enfoque de bolsa de palabras
library(rjson)
library(ggplot2)
library(zoo)
##
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
library(tm)
## Loading required package: NLP
##
## Attaching package: 'NLP'
## The following object is masked from 'package:ggplot2':
##
## annotate
library(SnowballC)
library(textcat)
library(caTools)
library(rpart)
library(rpart.plot)
library(randomForest)
## randomForest 4.6-14
## Type rfNews() to see new features/changes/bug fixes.
##
## Attaching package: 'randomForest'
## The following object is masked from 'package:ggplot2':
##
## margin
library(caret)
## Loading required package: lattice
library(e1071)
library(wordcloud)
## Loading required package: RColorBrewer
Sys.setlocale("LC_ALL", "C")
## [1] "C"
file_review = "../virajmehta/yelp_academic_dataset_review.json"
con = file(file_review, "r")
input <- readLines(con, 1000000L)
reviews <- as.data.frame(t(sapply(input,fromJSON)))
row.names(reviews) <- seq(1, nrow(reviews))
keeps <- c("date", "stars", "text", "business_id")
reviews = reviews[keeps]
reviews$date=as.Date.character(reviews$date, tryFormats = c("%Y-%m-%d"))
reviews$stars=as.numeric(reviews$stars)
reviews$text=as.character(reviews$text)
reviews[16:17,]
## date stars
## 16 2017-04-07 5
## 17 2015-01-03 4
## text
## 16 You can't really find anything wrong with this place, the pastas and pizzas are both amazing and high quality, the price is very reasonable, the owner and the staff are very friendly, if you're in downtown check this place out, a lot of people think just because it's downtown there are lots of options around but that's not always the case as there is also a lot of poor quality food in downtown as well.
## 17 Great lunch today. Staff was very helpful in assisting with selections and knowledgeable on the ingredients. We enjoyed the BBQ chicken with tika masala sauce and really good naan bread. The biryani with chicken was also yummy! Fun to see the food being prepared in the tandoori ovens. Great addition to the fast casual scene in Cleveland.
## business_id
## 16 YvrylyuWgbP90RgMqZQVnQ
## 17 NyLYY8q1-H3hfsTwuwLPCg
file_business = "../virajmehta/yelp_academic_dataset_business.json"
con2 = file(file_business, "r")
input2 <- readLines(con2, 500000L)
business <- as.data.frame(t(sapply(input2,fromJSON)))
row.names(business) <- seq(1, nrow(business))
keeps_business <- c("business_id", "name", "city", "stars", "review_count", "categories")
business = business[keeps_business]
business[21:22,]
## business_id name city stars review_count
## 21 YZCHr68c5aEVHz0bkq9K2g Park Stone Pavers Las Vegas 5 20
## 22 gJ5xSt6147gkcZ9Es0WxlA Rally's Hamburgers Cleveland 3 5
## categories
## 21 Home Services, Masonry/Concrete, Professional Services, Contractors
## 22 Fast Food, Burgers, Restaurants
business$is_restaurant = grepl("Restaurants", business$categories)
business = subset(business, is_restaurant == TRUE)
business[144:147,]
## business_id name city stars
## 453 QoXT0qI6_3WeHImUuLAyjg The George Street Diner Toronto 3.5
## 456 c4qE3ygdEQUD9dZjganavA Royal Ganesha Calgary 3.5
## 457 zvf4U6LJdNGOEogmmCOsLA Arby's Pittsburgh 3
## 459 dFuesggbkAFUUnKcihT3vA Bikinis Sports Bar & Grill Charlotte 2.5
## review_count
## 453 110
## 456 14
## 457 3
## 459 31
## categories
## 453 Restaurants, Breakfast & Brunch, Diners
## 456 Nightlife, Lounges, Food Delivery Services, Food, Restaurants, Bars, Indian
## 457 Restaurants, Fast Food
## 459 Restaurants, Burgers, Sports Bars, Bars, American (New), Nightlife, American (Traditional)
## is_restaurant
## 453 TRUE
## 456 TRUE
## 457 TRUE
## 459 TRUE
restaurant_reviews <- reviews[which(reviews$business_id %in% business$business_id),]
mean(business$review_count)
## Warning in mean.default(business$review_count): argument is not numeric or
## logical: returning NA
## [1] NA
ggplot(reviews, aes(x=stars))+
geom_bar(stat="bin", bins= 9, fill="violetred4") +
geom_text(stat='count', aes(label=..count..), vjust=1.6, color="white") +
ggtitle("Star Counts") +
xlab("Stars") + ylab("Count") +
theme_minimal()
#Preprocesamiento de texto
#Eliminar opiniones que no están en inglés
#Para comenzar, echemos un vistazo a en qué idioma están escritas las reseñas.
textcat(restaurant_reviews[20:30,]$text)
## [1] "english" "english" "english" "english" "english" "english" "english"
## [8] "english" "english" "english" "english"
restaurant_reviews = restaurant_reviews[1:50000,]
nrow(restaurant_reviews)
## [1] 50000
restaurant_reviews$language = textcat(restaurant_reviews$text)
#Echemos un vistazo más de cerca a la distribución de idiomas.
ggplot(restaurant_reviews, aes(x=language))+
geom_bar(stat="count", fill="violetred4") +
ggtitle("Language Count") +
xlab("Language") + ylab("Count") +
theme_minimal() + coord_flip()
#Como se esperaba, el inglés es, con mucho, el más común. Sin embargo, el escocés parece ser extremadamente popular también, lo cual es inesperado. Inspeccionaré las revisiones escocesas más de cerca, pero primero veamos cómo se ve una selección aleatoria de los otros idiomas.
foreignText = subset(restaurant_reviews, language !='english' & language !='scots')
foreignText[7:8,]
## date stars
## 1000 2015-07-03 4
## 1168 2016-09-18 3
## text
## 1000 Excellente place que vous passiez juste prendre un bon th<U+00E9> ou caf<U+00E9> ou que vous vouliez vous asseoir et manger un brownie d<U+00E9>cadent ou un grill cheese aux oignons caram<U+00E9>lis<U+00E9>s. Le personnel est sympathique, pas stress<U+00E9> et ne met pas de pression pour consommer. Les enfants sont les tr<U+00E8>s bienvenus et ont de quoi s occuper!!
## 1168 Je suis all<U+00E9> <U+00E0> quelques reprises aux 3 restos Ottavio et je prends toujours les Linguines Capiccole et poires je n'avais jamais <U+00E9>t<U+00E9> d<U+00E9><U+00E7>u mais hier sur Marcel Laurin elles <U+00E9>taient ordinaires... Pour le prix 19$ , ont-ils chang<U+00E9>s la recette?
## business_id language
## 1000 G7sVtpD6aqpuUB4F3LEG_w french
## 1168 oD8kSlXINk5u7d6HQh2SxQ french
scotishText = subset(restaurant_reviews, language =='scots')
scotishText[1:2,]
## date stars
## 54 2013-08-15 4
## 73 2015-06-21 4
## text
## 54 We had dinner here and the food was excellent and the service couldn't have been better!\nThanks Jose! \nWe will be back for sure.
## 73 received a mailer and thought okay let's go. walked in on a Monday afternoon greeted by a bartender wearing a cute t shirt that said the bar and below that the baby. how cute however never got her name. looked over menu asked what was good she said the club. so ordered it and yes it is good. had a beer a club on white toast served with fries and watched sport center. really felt at ease there and good music playing. will go back again.
## business_id language
## 54 eoyvbnRYQe-z85e8Rc6vAg scots
## 73 z9aXGRH8xtqpNDFE5_I3KA scots
restaurant_reviews = subset(restaurant_reviews, language =='english' | language =='scots')
#Crear variable de salida (sentimiento) a partir de la calificación de estrellas
#Tendría sentido asociar las reseñas de 4 y 5 estrellas con un sentimiento positivo y las revisiones de 1 y 2 estrellas con un sentimiento negativo.
#Las reseñas de 3 estrellas serían neutrales, pero por motivos de simplicidad, solo intentaremos predecir el sentimiento positivo y negativo, y volveremos a visitarlo después. Esto se debe a que nuestro objetivo es capacitar a un modelo para que reconozca un lenguaje positivo o negativo, y es probable que las revisiones de 3 estrellas contengan ambos. En lenguaje normal, el sentimiento neutral significaría que no estamos usando palabras con una emoción asociada, mientras que este no es el caso para las revisiones. Por el contrario, es probable que los clientes que asignan calificaciones de 3 estrellas hayan disfrutado algunos aspectos y no otros. Por esta razón, creo que incluir una categoría ’’ neutral ’’ probablemente afecte la precisión de nuestro modelo y prefiero descartarlo por ahora.
restaurant_reviews = subset(restaurant_reviews, stars != 3)
restaurant_reviews$positive = as.factor(restaurant_reviews$stars > 3)
table(restaurant_reviews$positive)
##
## FALSE TRUE
## 10325 32631
corpus = VCorpus(VectorSource(restaurant_reviews$text))
corpus = tm_map(corpus, content_transformer(tolower))
corpus = tm_map(corpus, removePunctuation)
corpus = tm_map(corpus, removeWords,stopwords("english"))
corpus = tm_map(corpus, stemDocument)
corpus[[1]]$content
## [1] "ill first admit excit go la tavolta food snob group friend suggest go dinner look onlin menu noth special seem overpr im also big order pasta go ala outnumb thank good order sea bass special die cook perfect season perfect perfect portion can say enough good thing dish server ask seem proud dish said doesnt chef incred job hubbi got crab tortellini also love heard mmmm good around tabl waiter super nice even gave us free dessert last peopl restaur servic slow place pack jug wine larg group good convers didnt seem bother anyon order calamari fri zucchini appet leav mussel sea bass special high recommend chicken parm crab tortellini also good big chicken romano bit bland hous salad teeni make reserv still expect wait food go larg group peopl plan loud dont go date unless your fight dont feel like hear anyth say ask sit side room avail"
frequencies = DocumentTermMatrix(corpus)
sparse = removeSparseTerms(frequencies, 0.99)
reviewsSparse = as.data.frame(as.matrix(sparse))
colnames(reviewsSparse) = make.names(colnames(reviewsSparse))
reviewsSparse$positive = restaurant_reviews$positive
set.seed(174)
split = sample.split(reviewsSparse$positive, SplitRatio = 0.7)
reviewsSparse$split = split
train = subset(reviewsSparse, split==TRUE)
test = subset(reviewsSparse, split==FALSE)
#nrow(train)
#nrow(test)
table(train$positive)
##
## FALSE TRUE
## 7227 22842
22903 /nrow(train)
## [1] 0.7616815
#Árbol de clasificación #Ahora construiré un modelo CART (esto significa Árboles de clasificación y regresión, en este caso será clasificación). Prefiero probar siempre este tipo de modelo antes de algo más complejo como un bosque aleatorio, porque es mucho más interpretable y se puede visualizar. De esta manera podremos ver qué palabras se trataron como predictores.
cartModel = rpart(positive ~ ., data=train, method="class")
prp(cartModel)
#Interpretación modelo #Podemos interpretar el modelo CART mirando el árbol resultante. Por ejemplo, si la revisión contiene lo peor al menos una vez, se etiquetará inmediatamente como negativa. Palabras como genial y delicioso conducen a críticas positivas.
#Vamos a evaluar el rendimiento de CART.
predictCART = predict(cartModel, newdata=test, type="class")
table(test$positive, predictCART)
## predictCART
## FALSE TRUE
## FALSE 1027 2071
## TRUE 268 9521
(1038 + 9564)/nrow(test)
## [1] 0.8226895
#El modelo CART tiene una mejora del 6% sobre el modelo de referencia, lo cual es un signo muy positivo. Además, si miramos el árbol, podemos concluir fácilmente que las palabras que usó para tomar decisiones son todas todas palabras relacionadas con sentimientos, aunque el modelo no fue entrenado usando un léxico de sentimientos.
#Mejora del modelo: validación cruzada
#Ahora intentaré mejorar el rendimiento alterando el número predeterminado de divisiones utilizadas para generar el árbol. La validación cruzada se proporciona en R para ayudar a elegir el número óptimo de divisiones. Existe un equilibrio entre demasiadas divisiones que corren el riesgo de sobreajustar el modelo y muy pocas que pueden no proporcionar una precisión lo suficientemente buena.
numFolds=trainControl(method = "cv", number = 10)
cpGrid = expand.grid(.cp=seq(0.001, 0.01, 0.001))
train(positive ~ ., data = train, method = "rpart", trControl = numFolds, tuneGrid = cpGrid)
## CART
##
## 30069 samples
## 785 predictor
## 2 classes: 'FALSE', 'TRUE'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold)
## Summary of sample sizes: 27062, 27062, 27062, 27061, 27063, 27063, ...
## Resampling results across tuning parameters:
##
## cp Accuracy Kappa
## 0.001 0.8606543 0.5994464
## 0.002 0.8548673 0.5900198
## 0.003 0.8513091 0.5752979
## 0.004 0.8509769 0.5726297
## 0.005 0.8362431 0.4883178
## 0.006 0.8232059 0.4103738
## 0.007 0.8199144 0.3897961
## 0.008 0.8159902 0.3704893
## 0.009 0.8158571 0.3712015
## 0.010 0.8158571 0.3712015
##
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was cp = 0.001.
#La validación cruzada me dio el parámetro óptimo cp = 0.001, así que ahora reconstruiré el árbol con este parámetro.
cartModelImproved = rpart(positive ~ ., data=train, method="class", cp= 0.001)
prp(cartModelImproved)
#En este caso, el árbol óptimo tiene muchas divisiones, lo que hace que sea más difícil de interpretar, pero nos da una buena visión general de qué palabras se usan para tomar decisiones divididas y, por lo tanto, qué palabras contribuyen más al sentimiento positivo / negativo. #Ahora obtengamos predicciones usando el nuevo árbol.
predictCARTImproved = predict(cartModelImproved, newdata=test, type="class")
table(test$positive, predictCARTImproved)
## predictCARTImproved
## FALSE TRUE
## FALSE 2047 1051
## TRUE 659 9130
(1789+9326)/nrow(test)
## [1] 0.8624971
#El modelo mejorado tiene una precisión del 86% en el conjunto de prueba.
#Esta es una mejora de más del 10% sobre la línea de base, y también para un modelo de PNL se considera una precisión muy alta. La razón principal por la que puedo obtener una precisión tan alta de los datos de texto es porque todo el texto está relacionado con el sentimiento. Esto es específico de los datos de revisión, si estaba mirando algo más, como el sentimiento de Twitter, muchas de las el texto sería fáctico y no expresaría opiniones. Por lo tanto, los datos de revisión son ideales para el análisis de sentimientos. #En segundo lugar, como estoy usando la calificación como predictor, esto es muy preciso. Algunos otros enfoques, como el uso de un léxico de sentimientos, sufren de las complejidades del lenguaje humano (por ejemplo, el humor y el sarcasmo pueden llevar a que se usen palabras positivas como negativas y viceversa).