Churn prediction

Evasão escolar

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]:
periodo bolsa repetiu ematraso disciplinas faltas desempenho abandonou
0 2 0.25 8 1 4 0 0.000000 1
1 2 0.15 3 1 3 6 5.333333 0
2 4 0.10 0 1 1 0 8.000000 0
3 4 0.20 8 1 1 0 4.000000 1
4 1 0.20 3 1 1 1 8.000000 0

In [138]:
df.describe()


Out[138]:
periodo bolsa repetiu ematraso disciplinas faltas desempenho abandonou
count 300.000000 300.000000 300.000000 300.000000 300.000000 300.000000 300.000000 300.000000
mean 5.460000 0.123333 2.776667 0.476667 2.293333 2.213333 2.623111 0.410000
std 2.937772 0.086490 2.530111 0.500290 1.648133 2.734853 2.583423 0.492655
min 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 3.000000 0.050000 0.000000 0.000000 1.000000 0.000000 0.400000 0.000000
50% 5.000000 0.100000 2.000000 0.000000 2.000000 1.000000 2.000000 0.000000
75% 8.000000 0.200000 5.000000 1.000000 4.000000 4.000000 4.000000 1.000000
max 10.000000 0.250000 8.000000 1.000000 5.000000 10.000000 10.000000 1.000000

In [139]:
features = df[['periodo','bolsa','repetiu','ematraso','disciplinas','faltas']]
labels = df[['abandonou']]

In [140]:
features.head()


Out[140]:
periodo bolsa repetiu ematraso disciplinas faltas
0 2 0.25 8 1 4 0
1 2 0.15 3 1 3 6
2 4 0.10 0 1 1 0
3 4 0.20 8 1 1 0
4 1 0.20 3 1 1 1

In [141]:
labels.head()


Out[141]:
abandonou
0 1
1 0
2 0
3 1
4 0

Separação dos dados

Precisamos separar os dados de teste dos dados de treino, virtualmente esquecendo que os dados de testes existem!


In [142]:
X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.33, random_state=42)

Padronização dos atributos

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]:
0 1 2 3 4 e
0 -0.480126 -0.860931 -1.060438 1.549659 -0.859126 1
1 1.218911 0.308306 2.091473 -0.869615 -0.505264 0
2 0.879104 0.892925 -0.666449 -0.264796 0.556319 1
3 0.539296 -0.860931 -0.666449 -1.474433 -0.859126 0
4 -1.159741 -0.860931 -1.060438 0.340022 0.202458 0

Kernel linear


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]:
0.6616915422885572

Kernel RBF com C=2 e sem gamma


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]:
0.78606965174129351

Kernel RBF com C=1 e gamma=10


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]:
1.0

Kernel Poly com C=2 e gamma=10


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]:
0.91542288557213936

Kernel Sigmoid com C=2 e gamma=100


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]:
0.5074626865671642

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]:
0 1 2 3 4 e
0 -1.159741 -0.860931 1.697485 0.340022 -0.859126 1
1 -0.140318 -0.276312 -1.060438 0.944841 0.556319 1
2 -0.480126 -0.276312 1.303496 -1.474433 -0.859126 0
3 -0.140318 0.308306 0.121529 -0.869615 -0.505264 0
4 -0.480126 -0.276312 2.091473 -0.869615 -0.859126 0

A melhor maneira de testar é comparar os resultados um a um.


In [159]:
predicoes = modeloRbfg10.predict(X_test_scaled)
printResults(predicoes)


Acertos 56
Percentual 0.5656565656565656
Erramos ao dizer que o aluno abandonou 43
Erramos ao dizer que o aluno permaneceu 0

In [178]:
predicoesGamma1 = modeloRbf.predict(X_test_scaled)
printResults(predicoesGamma1)


Acertos 68
Percentual 0.6868686868686869
Erramos ao dizer que o aluno abandonou 23
Erramos ao dizer que o aluno permaneceu 8

Este foi o melhor resultado que conseguimos.


In [187]:
predicoesPoly = modeloPoly.predict(X_test_scaled)
printResults(predicoesPoly)


Acertos 61
Percentual 0.6161616161616161
Erramos ao dizer que o aluno abandonou 19
Erramos ao dizer que o aluno permaneceu 19

In [177]:
predicoesSig = modeloSig.predict(X_test_scaled)
printResults(predicoesSig)


Acertos 54
Percentual 0.5454545454545454
Erramos ao dizer que o aluno abandonou 19
Erramos ao dizer que o aluno permaneceu 26

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)
Conclusão

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 [ ]: