Churn prediction é um tipo de trabalho muito comum em data science, sendo uma questão de classificação binária. Trada-se do possível abandono de um cliente ou de um aluno. Analisamos o dataset e tentamos prever as situações de risco de abandono, para tomarmos medidas proativas de fidelização. Para este exemplo, usamos um dataset real, porém desidentificado, de uma pesquisa que realizei há algum tempo, a pedido de uma instituição de ensino, para identificar os alunos com maior probabilidade de abandorarem o curso. Da população de alunos, foram eliminados os que possuem percentual de bolsa de estudos igual ou superior a 50%, pois trata-se de situações especiais. Os dados são coletados semanalmente, a partir dos resultados das primeiras provas de cada período.
In [136]:
import pandas as pd
import numpy as np
from sklearn import svm, datasets
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn import svm
In [137]:
df = pd.read_csv('evasao.csv')
df.head()
Out[137]:
In [138]:
df.describe()
Out[138]:
In [139]:
features = df[['periodo','bolsa','repetiu','ematraso','disciplinas','faltas']]
labels = df[['abandonou']]
In [140]:
features.head()
Out[140]:
In [141]:
labels.head()
Out[141]:
In [142]:
X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.33, random_state=42)
Como vamos usar SVM, precisamos colocar os atributos numéricos na mesma escala, e codificar os atributos de categoria. Temos um atributo de categoria: 'ematraso' e ele possui apenas dois valores: zero e um, logo, já está codificado. Se fossem múltiplos valores, teríamos que usar algo como o OneHotEncoder para transformá-lo em variáveis binárias.
In [179]:
padronizador = StandardScaler().fit(X_train[['periodo', 'bolsa', 'repetiu', 'disciplinas', 'faltas']])
X_train_1 = pd.DataFrame(padronizador.transform(X_train[['periodo', 'bolsa', 'repetiu', 'disciplinas', 'faltas']]))
X_train_scaled = pd.DataFrame(X_train_1)
X_train_scaled = X_train_scaled.assign(e = X_train['ematraso'].values)
X_train_scaled.head()
Out[179]:
In [144]:
modeloLinear = svm.SVC(kernel='linear')
modeloLinear.fit(X_train_scaled.values, y_train.values.reshape(201,))
modeloLinear.score(X_train_scaled.values, y_train.values.reshape(201,))
Out[144]:
In [145]:
modeloRbf = svm.SVC(kernel='rbf',C=2)
modeloRbf.fit(X_train_scaled.values, y_train.values.reshape(y_train.size))
modeloRbf.score(X_train_scaled.values, y_train.values.reshape(y_train.size))
Out[145]:
In [158]:
modeloRbfg10 = svm.SVC(kernel='rbf',C=1,gamma=10)
modeloRbfg10.fit(X_train_scaled.values, y_train.values.reshape(y_train.size))
modeloRbfg10.score(X_train_scaled.values, y_train.values.reshape(y_train.size))
Out[158]:
In [185]:
modeloPoly = svm.SVC(kernel='poly',C=2,gamma=10)
modeloPoly.fit(X_train_scaled.values, y_train.values.reshape(y_train.size))
modeloPoly.score(X_train_scaled.values, y_train.values.reshape(y_train.size))
Out[185]:
In [176]:
modeloSig = svm.SVC(kernel='sigmoid',C=2,gamma=100)
modeloSig.fit(X_train_scaled.values, y_train.values.reshape(y_train.size))
modeloSig.score(X_train_scaled.values, y_train.values.reshape(y_train.size))
Out[176]:
Nem sempre o modelo que tem melhor score é o modelo que dá a melhor previsão para dados ainda não vistos. Pode ser o caso de "overfitting". Vamos testar vários tipos de kernel e combinações de parâmetros.
In [180]:
X_test_1 = padronizador.transform(X_test[['periodo', 'bolsa', 'repetiu', 'disciplinas', 'faltas']])
X_test_1 = pd.DataFrame(padronizador.transform(X_test[['periodo', 'bolsa', 'repetiu', 'disciplinas', 'faltas']]))
X_test_scaled = pd.DataFrame(X_test_1)
X_test_scaled = X_test_scaled.assign(e = X_test['ematraso'].values)
X_test_scaled.head()
Out[180]:
A melhor maneira de testar é comparar os resultados um a um.
In [159]:
predicoes = modeloRbfg10.predict(X_test_scaled)
printResults(predicoes)
In [178]:
predicoesGamma1 = modeloRbf.predict(X_test_scaled)
printResults(predicoesGamma1)
Este foi o melhor resultado que conseguimos.
In [187]:
predicoesPoly = modeloPoly.predict(X_test_scaled)
printResults(predicoesPoly)
In [177]:
predicoesSig = modeloSig.predict(X_test_scaled)
printResults(predicoesSig)
In [126]:
def printResults(pr):
acertos = 0
errosAbandono = 0
errosPermanencia = 0
for n in range(0,len(pr)):
if pr[n] == y_test.values.flatten()[n]:
acertos = acertos + 1
else:
if pr[n] == 0:
errosAbandono = errosAbandono + 1
else:
errosPermanencia = errosPermanencia + 1
print('Acertos',acertos)
print('Percentual',acertos / len(pr))
print('Erramos ao dizer que o aluno abandonou', errosAbandono)
print('Erramos ao dizer que o aluno permaneceu', errosPermanencia)
No melhor modelo (RBF sem Gamma) acertamos 69% dos alunos que abandonaram o curso. Porém, nossos maiores erros foram ao prevermos que o aluno abandonou o curso, e erramos pouco ao considerar que ele não abandonou. Com isto, pecamos por excesso, mas conseguimos atingir mais os alunos que abandoraram ou abandonariam o curso, e isso é importante. Os erros mais relevantes seriam aqueles em que não consideramos o risco do aluno abandonar o curso, que foram poucos casos. Mesmo assim, alguns casos foram devidos a erro de registro da secretaria.
Um aluno foi considerado como tendo abandonado o curso quando não reabriu a matrícula por um determinado tempo. Na situação original, houve alguns casos de alunos que foram considerados como tendo abandonado o curso, mas, na verdade apenas demoraram a reabrir a matrícula. E houve casos em que o aluno realmente abandonou o curso, mas a secretaria ainda não havia registrado este fato.
In [ ]: