Analisando Sentimento em notas de app (Python) – Parte 2
Fala Pessoal, tudo bem?
Bom, recebi muitos feedbacks sobre o primeiro artigo, fico feliz que a galera está realmente tentando usar o código, que está conseguindo resolver o passo a passo!
Bom, se você chegou aqui e não viu a Parte 1, dá uma olhada, pois vamos continuar de onde parei!
O que foi feito até agora?
- Coleta de dados da Google Play – Reviews do App Rappi para entregadores e transformação para DataFrame.
- Normalização dos Dados: tokenização, remoção de stopwords e pontuações, lowercasing.
- Adição de Dicionário de Polaridade, para analisar palavra por palavra e sentimento correspondente. Rodamos isso e chegamos em duas colunas: Score_Sentimento (calcula o score da frase) e Sent, que apresenta o veredito, se é NEU (neutro), NEG (negativo), POS (positivo).
- E terminamos com uma WordCloud de todos os reviews, independente da nota.
2.1 Voltamos então à WordCloud.
“Rodrigão, legal, você montou uma WordCloud das reviews, então colocou tudo junto, negativa, positiva. E aí, como eu vou saber se a intenção das palavras na cloud são falar bem dos atributos descritos (tipo entrega, atendimento) ou se o usuário estava cornetando porque só dá problema?”
Say no more. Vamos te tirar dessa enrascada.
Já aviso que tem muitas brincadeirinhas Breaking Bad a caminho. Se quiser fugir delas, clique retornar no seu browser.
Então, vamos focar em criar as Wordclouds por tipo de sentimento.
Vamos dividir assim:
- Se o usuário deu notas 1 e 2, o sentimento é negativo.
- Se o usuário deu nota 3, o sentimento é neutro.
- Se o usuário deu nota 4 e 5, o sentimento é positivo.
Isso é um textbook da chamada Escala de Likert, que é a famosa escala de notas 1 a 5. Classifiquei assim também porque faz sentido.
Pode também classificar de acordo com a análise do SentiLex?
Pode. Mas nesse caso as classes ainda estão bastante desbalanceadas, então talvez saia um resultado diferente. Mas pode ser interessante num discovery e indicar para um insight que não pegamos aqui. É só mudar os objetos Negative, Positive e Neutral para esse parâmetro.
Se precisar de ajuda pra isso me manda um inbox que eu explico.
Primeiro, um pequeno passo apenas para sermos responsáveis com dados. Remover tudo que não nos interessa, id de usuário, nome, link para avatar de usuário.
Aqui eu escolhi apenas o que é relevante. Eu poderia também “dropar” o que não quero.
#Criando agora um Dataset apenas com o que vamos usar no modelo. Não preciso incluir nenhum detalhe sobre quem deu o review. Rappi = Reviews_Rappi[['content', 'thumbsUpCount', 'reviewCreatedVersion', 'at', 'score', 'Score_Sentimento', 'Sent']]
Bora pra primeira wordcloud:
#Realizando o mesmo processo, porém agora para avaliações negativas - notas 1 e 2 Negative = Rappi[Rappi.score < 3] Neg_Content = Negative['content'] all_neg_content = "".join(c for c in Neg_Content) wordcloud = WordCloud(stopwords=stopwords, background_color='orange', width=1600, height=800).generate(all_neg_content) fig, ax = plt.subplots(figsize=(16,8)) ax.imshow(wordcloud, interpolation='bilinear') ax.set_axis_off() plt.imshow(wordcloud)
E agora, temos um plot das avaliações negativas:
Idem para as neutras:
#Realizando o mesmo procedimento, para avaliações consideradas neutras (Nota = 3) Neutral = Rappi[Rappi.score == 3] Neu_Content = Neutral['content'] all_neu_content = "".join(c for c in Neu_Content) wordcloud = WordCloud(stopwords=stopwords, background_color='blue', width=1600, height=800).generate(all_neu_content) fig, ax = plt.subplots(figsize=(16,8)) ax.imshow(wordcloud, interpolation='bilinear') ax.set_axis_off() plt.imshow(wordcloud)
E agora as positivas:
#Finalmente, realizando o procedimento para notas chamadas Positivas, (4 e 5) Positive = Rappi[Rappi.score > 3] Pos_Content = Positive['content'] all_pos_content = "".join(c for c in Pos_Content) wordcloud = WordCloud(stopwords=stopwords, background_color='green', width=1600, height=800).generate(all_pos_content) fig, ax = plt.subplots(figsize=(16,8)) ax.imshow(wordcloud, interpolation='bilinear') ax.set_axis_off() plt.imshow(wordcloud)
Por que fez em laranja as negativas, azul as neutras e verde as positivas? Bom, eu achei que ficou bom assim e porque eu não sou muito fã de cor vermelha predominante num plot. Para trocar é só mexer ali no parâmetro background_color.
Vamos para os passos do….
2.2 Machine Learning: Pré processamento.
A proposta aqui é começarmos com um primeiro modelo. Esse modelo vai usar apenas duas colunas: content (o texto que o usuário escreveu) e score (a nota de 1 a 5 que está na Google Play).
Vamos ver o que acontece.
# Vetorização (Converter texto e números). from sklearn.feature_extraction.text import CountVectorizer vectorizer = CountVectorizer(max_features=1000) data_features = vectorizer.fit_transform(Rappi['content']) data_features = data_features.toarray()
Qual a pegada aqui?
Bom, o Python é um burrinho muito inteligente, e não consegue processar modelos se eles não forem variáveis numéricas. No caso do texto, você cria um vetor que representa o texto. Nesse caso eu usei o CountVectorizer. Ou seja, cada um dos tokens que eu criei lá atrás (correspondente a palavras), vira um valor numérico.
Aqui eu processei a coluna ‘content’, onde está presente o texto da avaliação.
Existem outras formas de Vectorizer, como por exemplo o TDIF, que a grosso modo calcula o peso de cada uma das palavras no seu dicionário para o texto em questão usando frequência do termo e frequencia inversa no corpus (dicionário).
Agora eu preciso processar a coluna objetivo, a coluna ‘score’.
labels = Rappi['score'].values
Agora o split.
O Split é assim: Vamos separar as 49 mil observações em train e test set.
- O train set é onde o algoritmo vai rodar e procurar os padrões. Vou usar 70% dos dados para isso.
- O test set é onde, a partir dos resultados eu comparo o que o algoritmo “chutou” versus o que aconteceu.
Nota: Se você faz modelos de Deep Learning é necessário que haja também um validation set, pois o modelo usa tentativa e erro para aprimorar os resultados. Isso cria um jogo de cartas marcadas chamado overfitting.
# Split data into training and testing set. from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(data_features, labels, test_size=0.3, random_state=42)
Aqui eu separei em 70% train e 30% test. Poderia usar até mais, isso é algo que pode mudar precisão do modelo.
Random State: 42. Isso é um número arbitrário. Basicamente quando usa-se essa fórmula, a separação 70%-30% é completamente aleatória. Mas, se você precisa replicar o resultado, você precisa de um aleatório replicável.
Eu sempre uso 42 por conta do Guia do Mochileiro das Galáxias, onde 42 é a resposta para o sentido da vida, do universo e tudo mais. Pode usar ‘123’ ou qualquer outro número, desde que ao replicar o resultado use o mesmo.
Seguindo.
2.3 Modelos de Machine Learning – Random Forest e Logistic Regression
Vamos usar o primeiro algoritmo de Machine Learning. O Random Forest Classifier. Depois vamos usar o Modelo Logistic Regression e comparar os resultados.
Para ficar interessante, vamos comparar com dois personagens do Breaking Bad.
Random Forest, o complicado, Walter White.
Costuma ser o rei dos algoritmos, super usado mas complexo.
Logistic Regression, o mais simples, Jesse Pinkman.
Na série, ambos produzem metanfetamina. O primeiro usa processos rebuscados e tem um grau alto de pureza. O segundo aprendeu na prática e, ao final da série chega a ser tão bom quanto seu mestre.
Random Forest, o Walter White.
O Random Forest é um algoritmo que usa diversas árvores de decisão limitadas a uma pequena porção do train set. Ele basicamente pega um pedaço aleatório do train set e avalia. Com esses múltiplos pedaços, ele chega numa resposta balanceada dos parâmetros.
Com isso, ele cria uma série de preditores fracos para chegar num preditor forte.
# Usando Random Forest para classificar os reviews. # Também calculando o Score Cross Validated. import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score forest = RandomForestClassifier(n_estimators=10, n_jobs=4) forest = forest.fit(X_train, y_train) print(forest) print(np.mean(cross_val_score(forest, data_features, labels, cv=10)))
Você roda esse código e espera uns 3 minutos. É isso que será respondido.
RandomForestClassifier(bootstrap=True, ccp_alpha=0.0, class_weight=None, criterion='gini', max_depth=None, max_features='auto', max_leaf_nodes=None, max_samples=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=4, oob_score=False, random_state=None, verbose=0, warm_start=False) 0.7162410572904553
0.7162 é o Precision Score após Validação. A validação é um instrumento para prevenir overfitting que é tornar um modelo tão preciso em si mesmo que ele prevê apenas ele mesmo.
Mas então isso quer dizer que o modelo acertou 71,62% das classificações? Sim.
Vamos ver isso em Detalhe com a Confusion Matrix.
result = forest.predict(X_test) import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix sns.set("poster") sns.set_style('whitegrid') conf_mat = confusion_matrix(y_test, result) cmap = sns.diverging_palette(220, 10, as_cmap = True) print(conf_mat) df_cm = pd.DataFrame(conf_mat, index = [i for i in "12345"], columns = [i for i in "12345"]) plt.figure(figsize = (10,7)) sns.heatmap(df_cm,cmap=cmap, annot=True, fmt='g').set_title('Confusion Matrix para Modelo Random Forest')
Com esse código temos a Confusion Matrix Assim:
A leitura é assim:
No eixo x, de 1 a 5, são as avaliações observadas no test set.
No eixo y, de 1 a 5 são as previsões que o modelo fez.
Logo, se você olhar na matriz a posição 1;1, significa quantas vezes o modelo previu o número 1 corretamente. em 1;2, quantas avaliações ele classificou como 2 e era 1. E assim por diante.
Daqui percebemos que o modelo é bem bom para prever nota 1 e nota 5. Para prever as intermediárias, não muito.
Logistic Regression, o Jesse Pinkman.
Esse aqui é certamente o modelo mais simples de Machine Learning. Mas é largamente usado, não é nada simplório!
O nome dele na Econometria e Estatística clássica é Logit, e faz parte da família dos chamados modelos não paramétricos.
Ele é tipo uma Regressão, mas não, pois a resposta dele não é numérica, mas classificatória.
Ou seja, ele não calcula por exemplo que a expectativa de vida de uma pessoa é 69,38 anos, que é o que uma regressão faz.
Ele separa as observações a partir das classificações disponíveis.
from sklearn.linear_model import LogisticRegression LogReg = LogisticRegression(max_iter = 10000) LogReg = LogReg.fit(X_train, y_train) print(LogReg) print(np.mean(cross_val_score(LogReg, data_features, labels, cv=10)))
Aqui é importante. Esse é um modelo mais lerdinho, pode levar até uns 10 minutos para ele rodar. Dá uma esticada nas pernas e volta para ver os resultados.
Depois desse tempo, você verá um resultado mais ou menos assim:
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, l1_ratio=None, max_iter=10000, multi_class='auto', n_jobs=None, penalty='l2', random_state=None, solver='lbfgs', tol=0.0001, verbose=0, warm_start=False) 0.727998749282969
72,79%?
Isso mesmo, aparentemente o LogReg ganhou!
Vejamos a Confusion Matrix:
result_logreg = LogReg.predict(X_test) conf_mat = confusion_matrix(y_test, result_logreg) cmap = sns.diverging_palette(120, 50, as_cmap = True) print(conf_mat) df_cm = pd.DataFrame(conf_mat, index = [i for i in "12345"], columns = [i for i in "12345"]) plt.figure(figsize = (10,7)) sns.heatmap(df_cm,cmap=cmap, annot=True, fmt='g').set_title('Confusion Matrix para Modelo Logistic Regression')
Interessante, a LogReg conseguiu prever melhor as notas 5 que o Random Forest, mas errou mais as notas 1.
Mas bem, para um Dataset que não teve um grande número de refinamentos, esse é um ótimo resultado! Nada mal!
2.4 Conclusão
Nesse artigo fiz uma continuação da parte exploratória dos dados, criando as wordclouds para cada uma das classificações de sentimento a partir das notas: 1 e 2, negativo; 3, neutro e 4 e 5, positivo.
Também mostrei uma das formas de processar texto em linguagem numérica para que seja possível rodar os algoritmos de Machine Learning.
Em seguida, apresentei dois modelos muito famosos, fiz piadinhas sem graça sobre eles e comparei os modelos.
Agora você, leitor atento, pode dizer:
“Poxa, mas todas aquelas firulas de SentiLex e as outras variáveis como data do review (afinal um app evolui), os “likes” (thumbs up) dos comentários mais relevantes e etc? Não usou no Machine Learning por que?”
É exatamente esses pontos que tentarei adicionar num modelo na parte 3.
Mas enquanto isso, eu realmente fiquei impressionado com os resultados, pois sei que é possível fazer muitos refinamentos, sobretudo no dicionário de palavras.
Como sempre, espero que tenham gostado e que tenha ajudado!
Agora é estudar esse caso, encontrar o seu próprio app de interesse e mandar ver para encontrar os resultados!
Um grande abraço!
Observações:
Agora é hora da segunda parte! Hora do Machine Learning! Hora da aventura! No artigo passado eu falei sobre como extrair comentários da Google Play via scraping, baixar um dicionário de sentimentos e calcular um score. Mostrei também como normalizar o dataset e depois apresentar visualmente numa wordcloud. Nessa parte 2, continuo onde parei:
– Faço wordclouds de acordo com a classificação de sentimento;
– Faço a preparação dos dados para o modelo usando vetorização;
– Boto 2 modelos clássicos pra brigar. E os resultados são bem interessantes! Espero que gostem! Um abraço!
1 Comentário
Parabens por esse conteudo!
Me ajudou demais, estou começando e ja tirou o meu medo de tentar e fracassar.
O mais massa é que fracassei durante 1 semana, e com o conjunto de dados que escolhi não passava as etapas.
Mas o mais incrivel aconteceu, eu consegui resolver o problema para fazer a tarefa ate o final pesquisando e estudando cada passo com a internet.
Muito obrigada.