Qualificação da interação dos clientes por meio das redes sociais - Análise Produto vs Marca

Este notebook trata-se de uma análise simples para classificação de texto, onde o objetivo é tentar diferenciar tópicos relacionados a produtos do banco com tópicos relacionados a marca somente utilizando dados do tweeter. O intuito do projeto é mostrar a capacidade que os dados de redes sociais tem em gerar um modelo satisfatório, o que para nós está acima do baseline de 64% (proporção Marca).

Este trabalho consiste de um esforço em grupo, e nenhum conteúdo apresentado aqui é de minha exclusividade e sim parte de um trabalho intenso de dois meses em que eu e meus colegas realizamos. Os responsáveis por este projeto são: Amanda Cavalcanti, Victor Ivo, Filipe Blaschi, Rafael Menezes e Cristiano Oliveira. Fica aqui meu agradecimento por ter feito parte deste time.

Códigos de pré-processamento dos dados e ideias dos modelos vieram do Cristiano, Cientista de Dados do Itaú.

Este notebook explica vagamente alguns conceitos, e o que eu entendo deles, para fontes mais seguras e de qualidade, por favor, acessar:

Início

Aqui já estamos com os dados processados, isto é, foi realizado a limpeza de caracteres indesejáveis, aplicado stemming e stopwords. Primeiro, vamos fazer uma visualização da estrutura do dataset:

import pandas as pd
import numpy as np

treino = pd.read_csv("base_treino.csv", sep=",")
teste = pd.read_csv("base_teste.csv", sep = ",")
treino.shape, teste.shape

((651, 3), (322, 3))

treino.head(5)
index conteudo CLASSIFICAÇÃO
0 223 vou procur outr banc segund abr cont pj vou ca... marca
1 805 rt lib googl pay prfv marca
2 832 ola consig pag bolet app vc manutenca la produto
3 152 log hoj cancel carta atend marca
4 1160 rt faz cobranc indev segund gerent reembols do... produto

Bag-of-Words

Bag-of-words trata-se de uma técnica onde transformamos vetores de palavras em vetores reais, requeridos pelos algoritmos que iremos utilizar. Como isto é feito? Cada palavra é tokenizada, isto é, cria-se uma coluna ou feature com a palavra e para cada linha indica-se se há ou não a presença desta no documento (tweet), seja com um vetor binário indicando presença/ausência, seja com a contagem destas. Porém, a ordem das palavras é perdida, o que pode acarretar em problemas pois a ordem tem implicações semânticas e sintáticas e contém informações que podem ser extremamente relevantes para a performance do classificador.

Vamos vetorizar o conteudo dos tweets utilizando a biblioteca TfIdfVectorizer. Tabelas tf-idf são utilizadas para dar importância à frequência da palavra por tweet (term-frequency), porém diminui o peso dela se a palavra aparece em muitos documentos (inverse-document frequency), pois assim ela torna-se mais irrelevante para tarefa de diferenciação dos tópicos, como por exemplo, artigos, pronomes, ou preposições, podem ser consideradas irrelevantes para a tarefa de classificação.

from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer

tfidf_model = TfidfVectorizer().fit(treino.conteudo.values.astype('U'))
X_treino = tfidf_model.transform(treino.conteudo.values.astype('U'))
X_teste = tfidf_model.transform(teste.conteudo.values.astype('U'))
y_treino = treino.CLASSIFICAÇÃO
y_teste = teste.CLASSIFICAÇÃO
X_treino.shape, y_treino.shape

((651, 1649), (651,))

Naive Bayes

Vamos aplicar o algoritmo Naive Bayes então, e verificar a performance do classificador na base de teste.

from sklearn.naive_bayes import MultinomialNB

clf = MultinomialNB()
clf.fit(X_treino, y_treino)
y_pred = clf.predict(X_teste)
from sklearn.metrics import precision_recall_fscore_support, accuracy_score
def scores(y_true, y_pred):
    avg_train_scores = precision_recall_fscore_support(y_true, y_pred, 
                                average = 'macro', 
                                labels = ['produto', 'marca'])
    scores_ = list(avg_train_scores)
    scores_.append(accuracy_score(y_true, y_pred))
    return pd.DataFrame(scores_, 
             columns = ['scores'],
             index = ['precision', 'recall', 'fscore', 'distribution', 'accuracy'])
scores(y_teste, y_pred)
scores
precision 0.777451
recall 0.599034
fscore 0.579611
distribution NaN
accuracy 0.708075

Vemos que temos um resultado satisfatório, isto é, acima do nosso baseline de 64%, que seria um modelo obtido escolhendo todas as classes como sendo Marca. Antes de procedermos com a utilização de algoritmos mais robustos como Support Vector Machines, vemos verificar como nosso modelo está com relação ao overfitting, e sua variância com relação as amostras retiradas de cada um. Para isso, vamos utilizar cross validation e o método do K-Fold.

# Vamos usar o RepeatedKFold, que gera KFold n vezes. Nosso objetivo é analisar a variância do modelo
from sklearn.model_selection import RepeatedKFold

def repeatedKfold(n_splits=3, n_repeats=3):
    rkf = RepeatedKFold(n_splits=n_splits, n_repeats=n_repeats)
    data_frame = []
    n = 1
    for train_index, test_index in rkf.split(X_treino):
        train, test = X_treino[train_index], X_treino[test_index]
        clf = MultinomialNB()
        clf.fit(train, y_treino[train_index])
        y_new_pred = clf.predict(test)
        data_frame.append(scores(y_treino[test_index], y_new_pred).T)
        
    return data_frame

df = pd.concat(repeatedKfold(3,5))
df.drop('distribution', inplace=True, axis=1)
df
precision recall fscore accuracy
scores 0.818152 0.591098 0.568898 0.718894
scores 0.755245 0.600996 0.584192 0.705069
scores 0.830625 0.662071 0.681974 0.792627
scores 0.791618 0.650110 0.664712 0.774194
scores 0.722716 0.582394 0.562025 0.700461
scores 0.768714 0.612582 0.604592 0.723502
scores 0.844828 0.590909 0.562009 0.709677
scores 0.735544 0.592228 0.579036 0.714286
scores 0.797496 0.638270 0.649548 0.769585
scores 0.752396 0.619572 0.623656 0.746544
scores 0.695875 0.554151 0.508120 0.663594
scores 0.831469 0.640347 0.649548 0.769585
scores 0.782868 0.606022 0.596285 0.728111
scores 0.780918 0.553339 0.498225 0.672811
scores 0.754367 0.645731 0.660513 0.769585

Calculando os valores médios, obtemos:

scores_df = pd.DataFrame({"Precisão":[df.precision.mean(), df.precision.std()],
                     "Recall": [df.recall.mean(), df.recall.std()],
                     "Acurácia": [df.accuracy.mean(), df.accuracy.std()]}, 
                         index = ["Media", "Desvio Padrao"])
scores_df
Acurácia Precisão Recall
Media 0.730568 0.777522 0.609321
Desvio Padrao 0.038590 0.042864 0.033418

Vemos um desvio padrão baixo e uma média de acurácia maior que o nosso baseline, portanto somos capazes de realizar melhor que um modelo aleatório.

Support Vector Machine

Vamos agora aplicar outro algoritmo, dito mais robusto e capaz de performar melhor em diversos tópicos que o Naive Bayes, Support Vector Machines ou SVM com kernek linear. Diferentemente do NB, entretanto, o SVM possui hiperparâmetros a serem ajustados, como o parâmetro C, ligado ao custo que cada slack variable gera para o modelo. Lembrando que um C alto indica que o modelo é mais restrito e o custo é alto para variáveis fora da margem de classificação, podendo causar overfitting. Já para valores baixos de C, o modelo não é tão restrito, e mais variáveis ultrapassam a margem de classificação, C menores evitam overfitting, porém podem causar underfitting. Portanto, a escolha do C ótimo torna-se essencial para o problema de classificação, e faremos isso utilizando o Grid Search, um método de busca forçada.

from sklearn.svm import LinearSVC
from sklearn.model_selection import GridSearchCV


parameters = {'C':[0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1,2,3,4,5, 10, 50, 100]}
svm = LinearSVC()
clf_gs = GridSearchCV(svm, parameters, cv=3)
clf_gs.fit(X_treino, y_treino)
clf_gs
clf_gs.best_params_

{‘C’: 2}

svm = LinearSVC(C=2)
svm.fit(X_treino, y_treino)
y_pred_svm = svm.predict(X_teste)

scores(y_teste, y_pred_svm)
scores
precision 0.692649
recall 0.664251
fscore 0.670923
distribution NaN
accuracy 0.717391

Vemos que a performance do SVM não é superior ao Naive Bayes, indicando possivelmente que precisamos de outras formas de análise ao invés de algoritmos diferentes. Vamos aplicar o conceito de n-grams e analisar os resultados.

N-gram

N-gram trata-se de um conceito onde tentamos preservar parte da ordem das palavras provindas dos documentos. Na utilização do bag-of-words, temos que cada palavra é transformada em um vetor real e sua ordem é perdida. Quando utilizamos então o termo n-gram, estamos tokenizando conjunto de palavras, definidos pela variável n, assim sendo, agora ao invés de cada coluna ser uma palavra, teremos colunar de duas ou três palavras, em forma de janelamento. Como exemplo:

"Tenho problemas no meu cartão de crédito"

BoW: - Tenho: 1 - problemas: 1 - no: 1 - meu: 1 - cartão: 1 - de: 1 - crédito: 1

2-grams: - Tenho problemas: 1 - problemas no: 1 - no meu: 1 - meu cartão: 1 - cartão de: 1 - de crédito: 1

# Criamos n-grams alterando o parâmetro ngram_range=(2,2) dentro da classe TfidfVectorizer.

tfidf_model = TfidfVectorizer(ngram_range= (2,2)).fit(treino.conteudo.values.astype('U'))
X_treino = tfidf_model.transform(treino.conteudo.values.astype('U'))
X_teste = tfidf_model.transform(teste.conteudo.values.astype('U'))
y_treino = treino.CLASSIFICAÇÃO
y_teste = teste.CLASSIFICAÇÃO
X_treino.shape, y_treino.shape

((651, 6043), (651,))

clf = MultinomialNB()
clf.fit(X_treino, y_treino)
y_pred = clf.predict(X_teste)
scores(y_teste, y_pred)
scores
precision 0.757686
recall 0.562319
fscore 0.519403
distribution NaN
accuracy 0.683230
svm = LinearSVC(C=2)
svm.fit(X_treino, y_treino)
y_pred_svm = svm.predict(X_teste)

scores(y_teste, y_pred_svm)
scores
precision 0.674078
recall 0.616425
fscore 0.616890
distribution NaN
accuracy 0.695652

Com 2-grams não vemos uma melhora clara na performance do classificador. Vamos utilizar o GridSearch novamente e verificar para mais valores e variando alguns outros parâmetros.

from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfTransformer, CountVectorizer

clf_pipeline = Pipeline([('vect', CountVectorizer()),
                     ('tfidf', TfidfTransformer()),
                     ('clf', LinearSVC())])
parameters = {'vect__ngram_range': [(1, 1), (1, 2), (2, 2), (2, 3), (1, 3)],
               'tfidf__use_idf': (True, False),
              'clf__C': [1,1.4,1.5,1.6, 2] }

gs_clf = GridSearchCV(clf_pipeline, parameters)
gs_clf.fit(treino['conteudo'], treino['CLASSIFICAÇÃO'])
gs_clf.best_params_

{‘clf__C’: 1, ‘tfidf__use_idf’: True, ‘vect__ngram_range’: (1, 2)}

A tupla de n_gram representada aqui significa que vamos tokenizar as palavras tanto uma a uma como com janelamento de n=2, assim, como demonstração:

import unidecode
import pprint

tweet = ["itau Já fui atendido e consegui resolver a situação. Mas obrigado pela presteza, o atendimento por aqui é sempre melhor?"]
tfidf_model = TfidfVectorizer(ngram_range= (1,2)).fit(tweet)
pprint.pprint(tfidf_model.vocabulary_)
{'aqui': 0,
 'aqui sempre': 1,
 'atendido': 2,
 'atendido consegui': 3,
 'atendimento': 4,
 'atendimento por': 5,
 'consegui': 6,
 'consegui resolver': 7,
 'fui': 8,
 'fui atendido': 9,
 'itau': 10,
 'itau já': 11,
 'já': 12,
 'já fui': 13,
 'mas': 14,
 'mas obrigado': 15,
 'melhor': 16,
 'obrigado': 17,
 'obrigado pela': 18,
 'pela': 19,
 'pela presteza': 20,
 'por': 21,
 'por aqui': 22,
 'presteza': 23,
 'presteza atendimento': 24,
 'resolver': 25,
 'resolver situação': 26,
 'sempre': 27,
 'sempre melhor': 28,
 'situação': 29,
 'situação mas': 30}

Assim, com o resultado obtido, vamos usar a base de testes para validação:

tfidf_model = TfidfVectorizer(ngram_range= (1,2)).fit(treino.conteudo.values.astype('U'))
X_treino = tfidf_model.transform(treino.conteudo.values.astype('U'))
X_teste = tfidf_model.transform(teste.conteudo.values.astype('U'))
y_treino = treino.CLASSIFICAÇÃO
y_teste = teste.CLASSIFICAÇÃO
X_treino.shape, y_treino.shape

svm_final = LinearSVC(C=1)
svm.fit(X_treino, y_treino)
y_pred_final = svm.predict(X_teste)
scores(y_teste, y_pred_final)
scores
precision 0.713559
recall 0.692271
fscore 0.698950
distribution NaN
accuracy 0.736025

Vemos um aumento na acurácia do classificador, não muito relevante, mas de uma forma satisfatória.

Conclusões

Como podemos ver, nosso resultado final foi um classificador com 73% de acurácia, o que está acima do baseline e cumpre muito bem o objetivo do projeto. Mostramos de forma quantitativa que é viável realizar uma distinção de tópicos somente utilizando dados de mídias sociais, agilizando o processo de tomada de decisão dos operadores e criando um fluxo de trabalho em que o atendimento é mais rápido e confiável.

Antes de finalizar, gostaria de prestar alguns pontos:

  • O classificador pode obter um resultado melhor com mais dados treinados.
  • Existem outros métodos a melhorar o algoritmo não citados aqui, bem como uma análise mais profunda dos motivos de o classificador se confundir, como técnica de redução de dimensionalidade, clusterização, engenharia de atributos mais robusta, entre outros.
  • Falando por mim, tive grandes dificuldades em classificar alguns tweets, talvez com especialistas no assunto tenhamos uma classificação mais real, o que ajuda em muito na tarefa.

Vocês podem verificar o notebook completo, bem como os dados utilizados no reposótio do github.

Dúvidas, críticas ou sugestões são muito bem-vindas.

Atualizado em: