Pular para o conteúdo principal

Machine learning com Kaggle: feature engineering

Aprenda como o feature engineering pode turbinar seus modelos de machine learning no Kaggle: crie novas colunas, transforme variáveis e muito mais!
· 14 min leer

Explore com IA

Nos dois tutoriais anteriores de Kaggle, você viu como preparar seus dados para criar seu primeiro modelo de machine learning, usando análise exploratória de dados e modelos básicos. Em seguida, você conseguiu construir seu primeiro modelo, um classificador de árvore de decisão. Você enviou esses modelos para o Kaggle e interpretou a acurácia.

Neste terceiro tutorial, você vai aprender mais sobre feature engineering, um processo em que você usa o conhecimento do domínio dos seus dados para criar recursos adicionais relevantes que aumentam o poder preditivo do algoritmo e fazem seus modelos de machine learning performarem ainda melhor!

Mais especificamente,

  • Primeiro você vai começar fazendo todos os imports necessários e carregando os dados no seu workspace;
  • Depois, vai ver por que vale a pena fazer feature engineering e começar a criar seus próprios recursos para o conjunto de dados! Você vai criar novas colunas, transformar variáveis em numéricas, tratar valores ausentes e muito mais.
  • Por fim, você vai construir um novo modelo de machine learning com seu novo conjunto de dados e enviá-lo ao Kaggle.

Hora de começar!

Antes de começar, faça os imports como no tutorial anterior, use um pouco de magia do IPython para garantir que as figuras sejam geradas inline no Jupyter Notebook e defina o estilo de visualização. Em seguida, importe seus dados e armazene a variável alvo do conjunto de treino em um lugar seguro. Depois, una os conjuntos de treino e teste (com exceção da coluna 'Survived' de df_train) e guarde o resultado em data.

Lembre-se de que isso garante que qualquer pré-processamento feito nos dados seja refletido tanto no treino quanto no teste!

Por fim, use o método .info() para dar uma olhada nos seus dados:

# Imports
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import re
import numpy as np
from sklearn import tree
from sklearn.model_selection import GridSearchCV

# Figures inline and set visualization style
%matplotlib inline
sns.set()

# Import data
df_train = pd.read_csv('data/train.csv')
df_test = pd.read_csv('data/test.csv')

# Store target variable of training data in a safe place
survived_train = df_train.Survived

# Concatenate training and test sets
data = pd.concat([df_train.drop(['Survived'], axis=1), df_test])

# View head
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 11 columns):
PassengerId    1309 non-null int64
Pclass         1309 non-null int64
Name           1309 non-null object
Sex            1309 non-null object
Age            1046 non-null float64
SibSp          1309 non-null int64
Parch          1309 non-null int64
Ticket         1309 non-null object
Fare           1308 non-null float64
Cabin          295 non-null object
Embarked       1307 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 122.7+ KB

Por que fazer feature engineering?

Você faz feature engineering para extrair mais informação dos seus dados e, assim, elevar o nível quando estiver construindo modelos.

Títulos dos passageiros do Titanic

Vamos ver um exemplo. Dê uma olhada na coluna 'Name' com a ajuda do método .tail(), que mostra as últimas cinco linhas dos seus dados:

# View head of 'Name' column
data.Name.tail()
413              Spector, Mr. Woolf
414    Oliva y Ocana, Dona. Fermina
415    Saether, Mr. Simon Sivertsen
416             Ware, Mr. Frederick
417        Peter, Master. Michael J
Name: Name, dtype: object

De repente, aparecem diferentes títulos! Ou seja, essa coluna contém textos com títulos como "Mr", "Master" e "Dona".

Esses títulos informam status social, profissão etc., o que no fim pode dizer algo sobre a sobrevivência.

À primeira vista, pode parecer difícil separar nomes de títulos, mas calma! Você pode usar expressões regulares para extrair o título e armazená-lo em uma nova coluna 'Title':

# Extract Title from Name, store in column and plot barplot
data['Title'] = data.Name.apply(lambda x: re.search(' ([A-Z][a-z]+)\.', x).group(1))
sns.countplot(x='Title', data=data);
plt.xticks(rotation=45);

gráfico de barras

Observação: essa nova coluna 'Title' é um novo feature do seu conjunto de dados!

Dica: para aprender mais sobre expressões regulares, confira meu artigo sobre nosso último evento de code along no FB Live ou o tutorial de expressões regulares em Python da DataCamp.

Você pode ver que há vários títulos no gráfico acima, e muitos aparecem raramente. Faz sentido agrupá-los em menos categorias.

Por exemplo, você provavelmente quer substituir 'Mlle' e 'Ms' por 'Miss' e 'Mme' por 'Mrs', pois são títulos franceses e, idealmente, você quer os dados em um único idioma. Em seguida, reúna títulos que não dá para categorizar de imediato e coloque-os em um grupo chamado 'Special'.

Dica: teste variações e veja como o algoritmo se comporta!

Depois, visualize o resultado com um barplot usando o método .countplot():

data['Title'] = data['Title'].replace({'Mlle':'Miss', 'Mme':'Mrs', 'Ms':'Miss'})
data['Title'] = data['Title'].replace(['Don', 'Dona', 'Rev', 'Dr',
                                            'Major', 'Lady', 'Sir', 'Col', 'Capt', 'Countess', 'Jonkheer'],'Special')
sns.countplot(x='Title', data=data);
plt.xticks(rotation=45);

gráfico de barras

É assim que fica seu feature recém-criado, 'Title'!

Agora, garanta que a coluna 'Title' existe e confira os dados novamente com o método .tail():

# View head of data
data.tail()
 PassengerIdPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedTitle
41313053Spector, Mr. WoolfmaleNaN00A.5. 32368.0500NaNSMr
41413061Oliva y Ocana, Dona. Ferminafemale39.000PC 17758108.9000C105CSpecial
41513073Saether, Mr. Simon Sivertsenmale38.500SOTON/O.Q. 31012627.2500NaNSMr
41613083Ware, Mr. FrederickmaleNaN003593098.0500NaNSMr
41713093Peter, Master. Michael JmaleNaN11266822.3583NaNCMaster

Cabines dos passageiros

Quando você carregou e inspecionou os dados, percebeu vários NaN ou valores ausentes na coluna 'Cabin'.

É razoável supor que esses NaN indiquem que a pessoa não tinha cabine, o que pode dizer algo sobre 'Survival'. Então, vamos criar a coluna 'Has_Cabin' para indicar se o passageiro tinha cabine ou não.

Observação: no código abaixo, o método .isnull() retorna True se o passageiro não tem cabine e False caso contrário. Mas como queremos armazenar em 'Has_Cabin' se o passageiro tem cabine, invertemos o resultado usando o til (~).

# Did they have a Cabin?
data['Has_Cabin'] = ~data.Cabin.isnull()

# View head of data
data.head()
 PassengerIdPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedTitleHas_Cabin
013Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNSMrFalse
121Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85CMrsTrue
233Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNSMissFalse
341Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123SMrsTrue
453Allen, Mr. William Henrymale35.0003734508.0500NaNSMrFalse

Agora, você vai remover algumas colunas que não trazem mais informação útil (ou que não sabemos como usar). Neste caso, estamos olhando para ['Cabin', 'Name', 'PassengerId', 'Ticket'], porque

  • Você já extraiu a informação sobre ter ou não cabine na coluna 'Has_Cabin';
  • Também já extraiu os títulos da coluna 'Name';
  • E vai descartar 'PassengerId' e 'Ticket' porque provavelmente não dizem nada sobre a sobrevivência dos passageiros do Titanic.

Dica: pode haver mais informação na coluna 'Cabin', mas, para este tutorial, vamos assumir que não!

Para remover essas colunas do seu DataFrame data, use o argumento inplace do método .drop() e defina como True:

# Drop columns and view head
data.drop(['Cabin', 'Name', 'PassengerId', 'Ticket'], axis=1, inplace=True)
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_Cabin
03male22.0107.2500SMrFalse
11female38.01071.2833CMrsTrue
23female26.0007.9250SMissFalse
31female35.01053.1000SMrsTrue
43male35.0008.0500SMrFalse

Parabéns! Você criou novos features como 'Title' e 'Has_Cabin' e removeu o que não agregava valor ao seu modelo!

Agora, vamos tratar valores ausentes, criar faixas para dados numéricos e transformar todos os recursos em variáveis numéricas usando .get_dummies() novamente. Por fim, você vai construir o modelo final deste tutorial. Confira nas próximas seções!

Tratando valores ausentes

Com todas as mudanças feitas no DataFrame data, é bom verificar se ainda restam valores ausentes com .info():

data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 9 columns):
Pclass       1309 non-null int64
Sex          1309 non-null object
Age          1046 non-null float64
SibSp        1309 non-null int64
Parch        1309 non-null int64
Fare         1308 non-null float64
Embarked     1307 non-null object
Title        1309 non-null object
Has_Cabin    1309 non-null bool
dtypes: bool(1), float64(2), int64(3), object(3)
memory usage: 133.3+ KB

O resultado mostra valores ausentes em 'Age', 'Fare' e 'Embarked'.

Lembre-se: dá para ver isso comparando o total de entradas (1309) com o número de valores não nulos listados por .info(). Aqui, 'Age' tem 1046 valores não nulos (263 ausentes). 'Fare' tem apenas um ausente e 'Embarked' tem dois.

Como no tutorial anterior, vamos imputar esses valores com .fillna():

Observação: usaremos a mediana para preencher 'Age' e 'Fare' por ser mais robusta a outliers. Outras opções são média ou moda.

Os dois valores ausentes de 'Embarked' serão preenchidos com 'S' (Southampton), o valor mais comum na coluna.

Dica: confirme isso fazendo um pouco mais de análise exploratória!

# Impute missing values for Age, Fare, Embarked
data['Age'] = data.Age.fillna(data.Age.median())
data['Fare'] = data.Fare.fillna(data.Fare.median())
data['Embarked'] = data['Embarked'].fillna('S')
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 9 columns):
Pclass       1309 non-null int64
Sex          1309 non-null object
Age          1309 non-null float64
SibSp        1309 non-null int64
Parch        1309 non-null int64
Fare         1309 non-null float64
Embarked     1309 non-null object
Title        1309 non-null object
Has_Cabin    1309 non-null bool
dtypes: bool(1), float64(2), int64(3), object(3)
memory usage: 133.3+ KB
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_Cabin
03male22.0107.2500SMrFalse
11female38.01071.2833CMrsTrue
23female26.0007.9250SMissFalse
31female35.01053.1000SMrsTrue
43male35.0008.0500SMrFalse

Criar faixas para dados numéricos

Agora vamos discretizar os dados numéricos, pois há uma faixa contínua de idades e tarifas. Pode haver flutuações que não refletem padrões reais e apenas ruído. Por isso, colocaremos pessoas dentro de determinadas faixas de idade ou tarifa no mesmo bin. Use a função qcut() do pandas para isso:

# Binning numerical columns
data['CatAge'] = pd.qcut(data.Age, q=4, labels=False )
data['CatFare']= pd.qcut(data.Fare, q=4, labels=False)
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_CabinCatAgeCatFare
03male22.0107.2500SMrFalse00
11female38.01071.2833CMrsTrue33
23female26.0007.9250SMissFalse11
31female35.01053.1000SMrsTrue23
43male35.0008.0500SMrFalse21

Observação: passamos as séries data.Age e data.Fare, definimos o número de quantis q=4 e usamos labels=False para codificar os bins como números.

Com as informações discretizadas, podemos remover as colunas 'Age' e 'Fare'. Não esqueça de conferir as primeiras linhas!

data = data.drop(['Age', 'Fare'], axis=1)
data.head()
 PclassSexSibSpParchEmbarkedTitleHas_CabinCatAgeCatFare
03male10SMrFalse00
11female10CMrsTrue33
23female00SMissFalse11
31female10SMrsTrue23
43male00SMrFalse21

Número de familiares a bordo

Outra ideia é criar uma nova coluna com o número de membros da família que estavam a bordo do Titanic. Neste tutorial, não vamos usá-la para avaliar o desempenho do modelo. Se quiser testar, execute a linha abaixo:

# Create column of number of Family members onboard
data['Fam_Size'] = data.Parch + data.SibSp

Por enquanto, vamos apenas remover as colunas 'SibSp' e 'Parch' do DataFrame:

# Drop columns
data = data.drop(['SibSp','Parch'], axis=1)
data.head()
 PclassSexEmbarkedTitleHas_CabinCatAgeCatFare
03maleSMrFalse00
11femaleCMrsTrue33
23femaleSMissFalse11
31femaleSMrsTrue23
43maleSMrFalse21

Transformar variáveis em numéricas

Agora que você criou recursos como 'Title' e 'Has_Cabin', tratou valores ausentes e discretizou os dados, é hora de transformar tudo em variáveis numéricas. Modelos de machine learning geralmente recebem entradas numéricas.

Como antes, use .get_dummies() para isso:

# Transform into binary variables
data_dum = pd.get_dummies(data, drop_first=True)
data_dum.head()
 PclassHas_CabinCatAgeCatFareSex_maleEmbarked_QEmbarked_STitle_MissTitle_MrTitle_MrsTitle_Special
03False001010100
11True330000010
23False110011000
31True230010010
43False211010100

Com tudo pronto, é hora de construir o modelo final!

Construindo modelos com seu novo conjunto de dados

Como antes, primeiro separe o data novamente em treino e teste. Depois, transforme-os em arrays:

# Split into test.train
data_train = data_dum.iloc[:891]
data_test = data_dum.iloc[891:]

# Transform into arrays for scikit-learn
X = data_train.values
test = data_test.values
y = survived_train.values

Agora, vamos treinar uma árvore de decisão no seu novo dataset com features engenheirados. Para escolher o hiperparâmetro max_depth, vamos usar uma variação do train-test split chamada "validação cruzada".

validação cruzada

Você começa dividindo o dataset em 5 grupos ou folds. Segura o primeiro fold como teste, ajusta o modelo nos quatro restantes, prevê no teste e calcula a métrica de interesse. Depois, repete segurando o segundo fold, e assim por diante com o terceiro, quarto e quinto.

Como resultado, você obtém cinco valores de acurácia, a partir dos quais pode calcular estatísticas como mediana, média e intervalos de confiança de 95%.

Você faz isso para cada valor de cada hiperparâmetro em ajuste e escolhe o conjunto que tem melhor desempenho. Isso é chamado de grid search.

Chega de teoria, vamos ao que interessa!

No próximo passo, você vai usar validação cruzada e grid search para escolher o melhor max_depth para o seu dataset com features engenheirados:

# Setup the hyperparameter grid
dep = np.arange(1,9)
param_grid = {'max_depth' : dep}

# Instantiate a decision tree classifier: clf
clf = tree.DecisionTreeClassifier()

# Instantiate the GridSearchCV object: clf_cv
clf_cv = GridSearchCV(clf, param_grid=param_grid, cv=5)

# Fit it to the data
clf_cv.fit(X, y)

# Print the tuned parameter and score
print("Tuned Decision Tree Parameters: {}".format(clf_cv.best_params_))
print("Best score is {}".format(clf_cv.best_score_))
Tuned Decision Tree Parameters: {'max_depth': 3}
Best score is 0.8103254769921436

Agora, faça previsões no conjunto de teste, crie a coluna 'Survived' e armazene suas previsões. Não esqueça de salvar as colunas 'PassengerId' e 'Survived' de df_test em um .csv e enviar ao Kaggle!

Y_pred = clf_cv.predict(test)
df_test['Survived'] = Y_pred
df_test[['PassengerId', 'Survived']].to_csv('data/predictions/dec_tree_feat_eng.csv', index=False)

kaggle

A acurácia do seu envio é 78,9.

Próximos passos

Veja se você consegue fazer mais feature engineering e testar novos modelos para melhorar essa pontuação. Este notebook, junto com os dois anteriores, está no GitHub. Seria ótimo ver você aprimorando esses modelos.

Há muito mais pré-processamento para aprender, como escalonamento dos dados. Os pipelines do scikit-learn também são super úteis. Confira nosso curso Supervised Learning with scikit-learn e a documentação do scikit-learn para isso e muito mais.

Temas

Relacionado

blog

O que é um modelo generativo?