Analisando Sentimento em notas de app (Python) – Parte 3
Fala Pessoal, tudo bem?
Finalmente chegamos à parte 3 da análise de Sentimento do App Rappi para Entregadores.
- Na parte 1, fizemos um scraping das reviews do app na Google Play, adicionamos um dicionário de sentimento (SentiLex-PT02), criamos uma coluna que dá o score de sentimento da frase pelas palavras usadas, usamos técnicas de normalização e criamos wordclouds;
- Na parte 2, continuamos com as wordclouds, fizemos o pré-processamento para o Machine Learning e usamos dois modelos para aferir precisão: Logistic Regression e Random Forest. Usamos somente o texto para prever a nota.
- Agora na parte 3, tentaremos melhorar a performance do modelo adicionando mais variáveis, como as de sentimento que criamos na parte 1.
3.1 Recapitulando a Parte 2.
A parte 2 teve um showdown entre dois modelos, os quais associei com personagens do Breaking Bad:
Random Forest, ou Walter White, mais complicado: 71,62% de precisão.
Logistic Regression, ou Jesse Pinkman, mais simples: 72,79% de precisão.
E a pergunta que permaneceu foi: legal, tratamos o texto do comentário das notas do app. Mas e todas as análises que fizemos, sobre sentimentos neutros, positivos e negativos. Será que esses também não podem contribuir para um modelo mais preciso?
Então vamos ver isso já!
3.2 Adicionando mais variáveis ao modelo simples (Comentário -> Nota)
E vamos fazer isso começando um novo modelo a partir do DataSet Rappi que eu criei na parte 2.
X = Rappi['content'] y = Rappi['score'] #Fazendo um novo split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 42)
Agora, vamos dar um nome para esse split, pois vamos fazer isso com os demais:
cvec = CountVectorizer(max_features = 1000).fit(X_train) #Vamos chamar o primeiro train set de df_train df_train = pd.DataFrame(cvec.transform(X_train).todense(), columns = cvec.get_feature_names()) df_test = pd.DataFrame(cvec.transform(X_test).todense(), columns=cvec.get_feature_names()) print(df_train.shape) print(y_train.shape) print(df_test.shape) print(y_test.shape)
E vamos seguir para cada uma das inclusões que quisermos fazer.
1) Inserindo a versão do app (afinal, com a evolução do app pode ser que a nota suba)
X = Rappi['reviewCreatedVersion'].apply(str) y = Rappi['score'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 42) cvec = CountVectorizer(max_features = 1000).fit(X_train) Version_train = pd.DataFrame(cvec.transform(X_train).todense(), columns = cvec.get_feature_names()) Version_test = pd.DataFrame(cvec.transform(X_test).todense(), columns=cvec.get_feature_names())
2) Inserindo os “likes” dos comentários, pois comentários com mais likes podem ser mais relevantes.
X = Rappi['thumbsUpCount'].apply(str) y = Rappi['score'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 42) cvec = CountVectorizer(max_features = 1000).fit(X_train) Thumbs_train = pd.DataFrame(cvec.transform(X_train).todense(), columns = cvec.get_feature_names()) Thumbs_test = pd.DataFrame(cvec.transform(X_test).todense(), columns=cvec.get_feature_names())
3) Inserindo a coluna de Sentimentos (Neutro, Positivo, Negativo) que calculamos após rodar o SentiLex na parte 1.
X = Rappi['Sent'].apply(str) y = Rappi['score'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 42) cvec = CountVectorizer(max_features = 1000).fit(X_train) Sent_train = pd.DataFrame(cvec.transform(X_train).todense(), columns = cvec.get_feature_names()) Sent_test = pd.DataFrame(cvec.transform(X_test).todense(), columns=cvec.get_feature_names())
Inserindo a Data de Review.
X = Rappi['at'].apply(str) y = Rappi['score'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 42) cvec = CountVectorizer(max_features = 1000).fit(X_train) at_train = pd.DataFrame(cvec.transform(X_train).todense(), columns = cvec.get_feature_names()) at_test = pd.DataFrame(cvec.transform(X_test).todense(), columns=cvec.get_feature_names())
Então agora nosso modelo ficou:
O que queremos prever: Nota do App
A partir de: Comentários, Data de Review, Sentimento do Comentário, Versão do App, Likes no Comentário.
E agora, o temperinho especial! Vamos concatenar o set e uma única matriz para realizar a análise.
Ou seja, vamos concatenar os train sets e test sets que criamos para gerar um novo set.
train = pd.concat ([df_train, Sent_train, Thumbs_train, Version_train, at_train], axis = 1) test = pd.concat([df_test, Sent_test, Thumbs_test, Version_test, at_test], axis = 1) print(train.shape) print(test.shape) print(y_train.shape) print(y_test.shape)
Lembrando, adicionar mais variáveis a um modelo pode tanto ajudá-lo como piorá-lo, já que aumentando a dimensionalidade das coisas você pode comprometer a capacidade de previsão de um modelo.
Vejo muita, mas muita, mas MUITA gente caindo nessa falácia, de que o modelo cheio de variáveis explica melhor.
Vamos ver o que aconteceu?
3.3 Voltando aos modelos
Vamos usar os mesmos modelos, Random Forest e Logistic Regression para podermos comparar os resultados anteriores.
forest = RandomForestClassifier(n_estimators=10, n_jobs=4) forest = forest.fit(train, y_train) print(forest) print(np.mean(cross_val_score(forest,test, y_test, cv=10)))
E o modelo retorna….
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.7285819698604094
72.85% versus 71.62% que chegamos na parte 2, usando apenas texto.
Agora, vamos verificar na Logistic Regression
LogReg = LogisticRegression(max_iter = 10000) LogReg = LogReg.fit(train, y_train) print(LogReg) print(np.mean(cross_val_score(LogReg, test, y_test, cv=10)))
E o modelo retorna…
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.7302724384046931
73,02% versus 72,79% na parte 2.
Ou seja, as variáveis adicionadas melhoraram somente 1% da previsão.
Wesley Safadão mandou lembranças.
3.4 Conclusão.
Bom, foi um pouco triste, não? Crescer pouco na capacidade de previsão!
Mas isso também mostra três coisas importantes:
1) Para melhorar modelos de NLP, não tem muita escapatória: seus dicionários têm que ser muito bons, de preferência customizados para o problema que você vai atacar. Como o propósito dessa análise foi ganhar familiaridade e fazer um artigo a respeito, não me aprofundei nessa parte pois requer análise aprofundada e torna-se bastante custoso.
Mas, caso alguém queira fazer, o que eu faria?
- Analisaria as palavras por frequência a partir da primeira tokenização e verificaria se há possibilidade de padronização melhor. Tipo não = nao = naum.
- Atualizaria o SentiLex para palavras que aparecessem com frequência específica em cada uma das notas.
- Tornaria mais rico o dicionário de stopwords, o qual usamos praticamente o padrão do nltk em português.
Um clássico dizer que se usa em Data é: garbage in, garbage out.
2) Determinar que uma coisa não influi (ou influi pouco) na outra é também uma resposta! Isso quer dizer que uma das principais coisas que você tem que fazer quando defende um modelo (principalmente um modelo “lean”) é a pergunta: “mas por que você não adicionou a variável A, B, C, afinal elas podem influir muito no modelo etc etc”. Acredite em mim: nessa hora todo mundo vira cientista de dados e tem uma opinião! Ou seja, além de explicar o que você usou, é muito importante explicar o que você não usou e por que.
3) No nosso caso, ainda que trabalhoso, ainda aumentamos a precisão de ambos os modelos, e ficou assim:
- Primeiro Lugar: LogReg com todas as Features: 73.02%
- Segundo Lugar: Random Forest com todas as Features: 72,85%
- Terceiro Lugar: LogReg apenas com a feature Comentário da Nota: 72,79%
- Quarto Lugar: Random Forest apenas com a feature Comentário da Nota: 71,62%
Eu, pessoalmente, optaria por usar Logistic Regression para uma melhoria do modelo, justamente por uma regra muito muito muito importante:
No caso de empate entre dois modelos, escolha sempre o mais simples.
E no caso, tanto do último lugar ao primeiro, temos aí um empate técnico. E nesse sentido optaria sempre pelo mais simples, mesmo que ele fosse o que apresentasse precisão pior dentro desse range. Se fosse 70% versus 95%? Aí claro que não.
Chegamos ao fim da saga!
Foi uma super aventura pra mim, águas desconhecidas no NLP em português!
Tentei fazer um caminho simples e reprodutível para a maioria das pessoas que está ganhando mais conforto no Python para Data Science.
Tomando esse código como ponto de partida, eu tenho certeza que é possível evoluir e muito a precisão e quem sabe, transformar isso num produto de dados!
Um grande abraço!
Comentários:
Na parte 1 foi a vez de puxar os dados via scraping, normalizá-los, criar um score de sentimento e visualizar os dados.
Na parte 2, pré-preparar os dados e colocar em uso dois modelos de Machine Learning concorrentes usando apenas o texto do comentário para prever as notas do app.
Agora na parte 3, vamos usar, além do texto, outras variáveis como:
– Score de Sentimento, calculado na parte 1;
– Data da Review;
– Relevância da Review (calculado pelo numero de likes);
E depois disso, retornar aos modelos e ver como eles respondem. Espero que gostem!
Tag:#machinelearning, #nlp, #python