import pandas as pd
import re
from datetime import datetime
import string
from nltk.tokenize import word_tokenize
import nltk
import matplotlib.pyplot as plt
nltk.download('punkt')
nltk.download('stopwords')
stopwords = nltk.corpus.stopwords.words('portuguese')
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.preprocessing import FunctionTransformer
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.metrics import f1_score, matthews_corrcoef, accuracy_score
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.multiclass import OneVsRestClassifier
[nltk_data] Downloading package punkt to /root/nltk_data... [nltk_data] Unzipping tokenizers/punkt.zip. [nltk_data] Downloading package stopwords to /root/nltk_data... [nltk_data] Unzipping corpora/stopwords.zip.
dados = pd.read_csv('/content/drive/MyDrive/DATA/pedidos-recursos-features-process.csv')
def clear_values_regex(df):
df = df.replace(r'\r\n\r\n',' ', regex=True)
df = df.replace(r'\r\n',' ', regex=True)
df = df.replace(r'\r \r',' ', regex=True)
return df
dados = clear_values_regex(dados)
dados['conteudo_resposta'] = dados['conteudo_resposta'].fillna('')
dados = dados[dados['atendimento_resposta'] != 'Não Classificado']
def limpando_texto(mensagem):
# Diminuindo os caracteres
mensagem = mensagem.lower()
# url
regex_url = r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))"
mensagem = re.sub(regex_url,'site',mensagem)
# data replace
mensagem = re.sub(r'\d{2}.\d{2}.\d{4}','data',mensagem)
# remove pontuação
table = str.maketrans('', '', string.punctuation)
stripped = [palavra.translate(table) for palavra in mensagem.split()]
mensagem = ' '.join(stripped).strip()
# remove stopword
mensagem_tokens = word_tokenize(mensagem)
mensagem = [word for word in mensagem_tokens if not word in stopwords]
mensagem = " ".join(mensagem)
return mensagem
dados['conteudo_pedido'] = dados['conteudo_pedido'].apply(limpando_texto)
dados['conteudo_resposta'] = dados['conteudo_resposta'].apply(limpando_texto)
Para o nosso primeiro modelo, iremos considerar apenas as informações relacionado ao pedido e a primeira resposta que órgão forneceu.
Considerando que a modelagem matemática só consegue ter um entendimento em relação a números fica inviável passar a ‘string’ com as palavras na forma que entendemos, portanto, para a representação dos textos presente no problema iremos utilizar a técnica TF-IDF:
O valor tf–idf (abreviação do inglês term frequency–inverse document frequency, que significa frequência do termo–inverso da frequência nos documentos), é uma medida estatística que tem o intuito de indicar a importância de uma palavra de um documento em relação a uma coleção de documentos ou em um corpus linguístico.
O valor tf–idf de uma palavra aumenta proporcionalmente à medida que aumenta o número de ocorrências dela em um documento, no entanto, esse valor é equilibrado pela frequência da palavra no corpus. Isso auxilia a distinguir o fato da ocorrência de algumas palavras serem geralmente mais comuns que outras.
Para essa etapa iremos utilizar um modelo SVM:
Uma máquina de vetores de suporte (SVM, do inglês: support vector machine) é um conceito na ciência da computação para um conjunto de métodos de aprendizado supervisionado que analisam os dados e reconhecem padrões, usado para classificação e análise de regressão. O SVM padrão toma como entrada um conjunto de dados e prediz, para cada entrada dada, qual de duas possíveis classes a entrada faz parte, o que faz do SVM um classificador linear binário não probabilístico. Dados um conjunto de exemplos de treinamento, cada um marcado como pertencente a uma de duas categorias, um algoritmo de treinamento do SVM constrói um modelo que atribui novos exemplos a uma categoria ou outra.
Aplicamos a definição do pipline de treinamento do modelo, que consiste em uma instaciação do modelo SVM com os paramentros : {C = 100 , gamma=0.001 , kernel='rbf'}
def get_text_data(x):
return [record[0] for record in x]
def get_text_data_2(x):
return [record[1] for record in x]
transformer_text = FunctionTransformer(get_text_data)
transformer_text_2 = FunctionTransformer(get_text_data_2)
def train_models(X_train,y_train):
pipeline = Pipeline([
('features', FeatureUnion([
('text_features', Pipeline([
('selector', transformer_text),
('vec', TfidfVectorizer(analyzer='word'))
])),
('text_features2', Pipeline([
('selector', transformer_text_2),
('vec', TfidfVectorizer(analyzer='word'))
]))
])),
('svc', SVC(C = 100 , gamma=0.001 , kernel='rbf'))
])
pipeline.fit(X_train,y_train)
return pipeline
Para aplicar o treinamento do modelo iremos realizar a filtragem nos dados, para considerar apenas o pedido, resposta e sua respectiva classe.
dados = dados[['conteudo_pedido','conteudo_resposta','atendimento_resposta']]
Após esse processo iremos dividir nosso conjunto em 80% de Treino e 20% de teste para podermos avaliar nosso modelo, portanto temos:
def label_encode(x):
if x == 'Atendido':
return 1
else:
return 0
def split_data(df):
labels = df['atendimento_resposta'].apply(label_encode)
df.drop(columns=['atendimento_resposta'])
X_train, X_test, y_train , y_test = train_test_split(df, labels, test_size=0.2, random_state=5)
X_train = X_train.to_numpy()
X_test = X_test.to_numpy()
return X_train, X_test, y_train , y_test
Esse processo também conta com a transformação das variavel algo em encondes de acordo com os cénarios:
Esse processo é pra ajudar o modelo no melhor entendimento sobre os dados.
No nosso contexto, dado que a variável de interesse é o atendimento do pedido, temos:
Falso Positivo (FP): O modelo previu que o pedido foi atendido, entretanto, não foi.
Falso Negativo (FN): O modelo previu que o pedido não foi atendido, entretanto, ele foi.
Verdadeiro Negativo (VN): O modelo previu que o pedido não foi atendido, e de fato ele não foi.
Verdadeiro Positivo (VP): O modelo previu que o pedido foi atendido, e de fato ele foi.
Para ajudar na compreensão do comportamento do modelo, iremos utilizar as seguintes técnicas de avaliação:
VP + VN / (VP + FP + VN + FN)
F1-Score
recall = VP / (VP + FN)
precision = VP / (VP + FP)
F1 = 2 (precision recall) / (precision + recall)
MCC
MCC = VPVN - FPFN/ $\sqrt{(VP+FP)(VP+FN)(VN+FP)(VN+FN)}$
Para o nosso problema considerando o balanceamento das classes e problema binário, a métrica MCC serve como boa referência porque ela avalia todos os cenários possíveis para a classificação e possui um valor ajustado de fácil entendimento, em que: -1: quanto mais próximo de -1 sua MCC, pior está o seu classificador.
Portanto, temos:
def test(df):
X_train, X_test, y_train , y_test = split_data(df)
model = train_models(X_train,y_train)
predicted_text = model.predict(X_test)
accuracy = accuracy_score(y_test, predicted_text)
f1 = f1_score(y_test, predicted_text)
mcc = matthews_corrcoef(y_test, predicted_text)
result = pd.DataFrame(zip([float('{:04.2f}'.format(accuracy*100))],[float('{:04.2f}'.format(f1*100))],[mcc]),columns=['Acurácia','F1','MCC'])
return result
Para esse processo iremos considerar o nosso melhor modelo atualmente o SVM em dois diferentes cenários:
O objetivo dessa atividade é mensurar o comportamento do modelo nas diferentes classes e consequentemente expandir os dados além do que se vinha modelando.
def cenarios_teste(data):
key = ['Atendido vs Não Atendido','Atendido vs (Não e Parcialmente Atendido)']
cenarios = {
'Atendido vs Não Atendido' : ['Atendido','Não Atendido'],
'Atendido vs (Não e Parcialmente Atendido)' : ['Atendido','Não Atendido','Parcialmente Atendido']
}
result_final = pd.DataFrame(columns=['cenario','Acurácia','F1','MCC'])
for k in key:
local = data[data['atendimento_resposta'].isin(cenarios[k])]
result_local = test(local)
result_local['cenario'] = k
result_final = pd.concat([result_final,result_local])
return result_final
resultados = cenarios_teste(dados)
resultados
cenario | Acurácia | F1 | MCC | |
---|---|---|---|---|
0 | Atendido vs Não Atendido | 81.81 | 87.08 | 0.588660 |
0 | Atendido vs (Não e Parcialmente Atendido) | 73.57 | 78.37 | 0.468689 |
Observamos bom resultado para esse modelo considerando que ele só utiliza duas informações que é pedido e resposta, um ponto importante é que o MCC para ambos os casos estão tendendo a 1 isso nos mostra que está no caminho coerente.
Considerando o cenário Atendido vs (Não e Parcialmente Atendido) podemos imaginar que os Parcialmente pode estar indo para às duas classes nas considerações do modelo, as próximas etapas são considerar um cenário com às três categorias e comparar o resultado.