← Voltar pra playlist

O Titanic: Meu Primeiro Dado Sujo de Verdade

Aula 7, e o professor muda de regime: até aqui todo dataset já chegava pronto, sem buraco, sem coluna inútil, sem texto solto. O Titanic (o dataset mais famoso do Kaggle, provavelmente o primeiro projeto de todo mundo que começa em ciência de dados) não tem esse luxo.

O dado sujo: 891 passageiros, nem toda coluna serve

df = pd.read_csv('train.csv')
y = df['Survived']
X = df.drop('Survived', axis=1)

891 passageiros, Survived (0 ou 1) como alvo, e um punhado de colunas de entrada bem misturadas: Pclass (classe do bilhete), Name, Sex, Age, SibSp (irmãos/cônjuge a bordo), Parch (pais/filhos a bordo), Ticket, Fare, Cabin, Embarked (porto de embarque). O professor descarta quatro delas de cara:

caracteristicas_indesejadas = ['PassengerId', 'Name', 'Ticket', 'Cabin']

PassengerId é só um número sequencial, não carrega informação nenhuma sobre a pessoa. Name e Ticket são texto livre, quase todo valor é único (891 nomes diferentes pra 891 passageiros), então um modelo não tem como generalizar nada a partir disso sem processar o texto primeiro (extrair título, tipo "Mr."/"Mrs.", seria um jeito válido, mas é trabalho extra que a aula não fez). Cabin falta em boa parte das linhas e também é bem específica demais.

Dado faltando é a regra, não a exceção

for col in Xnum.columns:
    print(f"{col:>12} {Xnum[col].isnull().sum():2}")

Saída: Age falta em 177 dos 891 passageiros (quase 20%). Embarked falta em 2.

Isso nunca tinha acontecido nessa playlist até agora: todo dataset anterior já vinha completo. Aqui, quase 1 em cada 5 idades é NaN. Descartar essas linhas jogaria fora 20% dos dados, e descartar a coluna Age inteira jogaria fora uma variável provavelmente importante (criança tinha prioridade no resgate). A solução do professor é imputação: preencher o buraco com um valor estimado.

from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy='median')
XnumTratado = imputer.fit_transform(Xnum)

Pra variáveis numéricas, a mediana (não a média): a distribuição de idade e de tarifa (Fare) tem gente bem fora da curva (bebê de meses, tarifas de primeira classe bem altas), e a mediana não se deixa puxar por esses extremos do jeito que a média se deixa. Pra variáveis categóricas (Embarked), não existe "mediana" de porto, então a estratégia vira 'most_frequent', o valor que mais aparece.

Categoria não é número, mesmo quando parece

Sex e Embarked são texto ("male"/"female", "S"/"C"/"Q"). Um modelo só entende número, mas simplesmente numerar (male=0, female=1, ou S=0, C=1, Q=2) inventaria uma ordem que não existe: por que "Q" seria "maior" que "S"? O jeito certo é o one-hot encoding: cada categoria vira sua própria coluna binária.

from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder()
XcatTratadoHot = encoder.fit_transform(XcatTratado)

Saída: as 2 colunas categóricas (Sex, Embarked) viram 5 colunas binárias (2 pra sexo, já que uma sobra redundante com a outra, e 3 pra porto de embarque). Nenhuma delas tem uma ordem numérica implícita, só "é" ou "não é" aquela categoria.

Meu próprio Transformer: nem tudo é classificador

Até aqui, toda classe própria dessa playlist herdava de ClassifierMixin ou RegressorMixin, porque sempre terminava em .predict(). Aqui o objetivo é diferente: só transformar o dado, sem prever nada. Pra isso existe o TransformerMixin:

from sklearn.base import BaseEstimator, TransformerMixin

class AtributosDesejados(BaseEstimator, TransformerMixin):
    def __init__(self):
        self.colunas_indesejadas = ['PassengerId', 'Name', 'Ticket', 'Cabin']
    def fit(self, X, y=None):
        return self
    def transform(self, X, y=None):
        return X.drop(self.colunas_indesejadas, axis=1)

Junto com AtributosNumericos (separa só as colunas numéricas) e AtributosCategoricos (separa só as categóricas), o professor monta dois Pipeline paralelos, um pra cada tipo de dado:

pipenum = Pipeline([
    ('atributos_numericos', AtributosNumericos()),
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])
pipecat = Pipeline([
    ('atributos_categoricos', AtributosCategoricos()),
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder())
])

E o FeatureUnion junta a saída dos dois num só conjunto de colunas, lado a lado:

from sklearn.pipeline import FeatureUnion

unecaracteristicas = FeatureUnion([
    ('pipenum', pipenum),
    ('pipecat', pipecat)
])

Saída: as 7 colunas originais (depois de descartar as 4 inúteis) viram 10 colunas tratadas: 5 numéricas (já imputadas e normalizadas) mais 5 categóricas (já imputadas e viradas one-hot).

O pipeline final encadeia tudo, do dado cru até o classificador, numa única chamada de .fit():

preproc = Pipeline([
    ('atributos_desejados', AtributosDesejados()),
    ('unecaracteristicas', unecaracteristicas),
    ('to_dense', DenseTransformer())
])

clf = Pipeline([
    ('preproc', preproc),
    ('classificador', RandomForestClassifier())
])
clf.fit(X, y)

O mesmo erro de sempre, com consequência de verdade

y_pred = clf.predict(X)
accuracy_score(y, y_pred)

Saída: 0.9798. Quase 98% de acerto.

Se você já leu o post sobre validação cruzada dessa playlist, essa acurácia deveria acender um alerta: é medida no mesmo dado que treinou. A prova de que é decoreba, não aprendizado:

from sklearn.model_selection import cross_val_score
scores = cross_val_score(clf, X, y)

Saída: 0.807 de média (0.765, 0.815, 0.854, 0.775, 0.826 nas 5 dobras).

Uma queda de quase 17 pontos percentuais entre "acurácia no treino" e "acurácia honesta". A Random Forest, sem limite de árvore nenhum, memoriza boa parte dos 891 passageiros individualmente, e o número de 98% não media capacidade de generalizar, media capacidade de decorar.

Tentando bater o 80%: stacking e uma rede neural

O professor tenta duas coisas pra melhorar o 0.807 honesto. Primeiro, StackingClassifier com 8 modelos bem diferentes entre si de uma vez (Random Forest, Extra Trees, uma rede neural, SGD, Ridge, KNN, Naive Bayes, regressão logística):

Saída: 0.824 de validação cruzada. Uma melhora real, ainda que pequena.

Depois, uma rede neural sozinha (MLPClassifier, sigla de Multi-Layer Perceptron, um Perceptron que eu já vi antes nessa playlist empilhado em várias camadas, o assunto de uma matéria própria que ainda não é o foco aqui):

Saída: 0.822 de validação cruzada, praticamente empatada com o stacking de 8 modelos.

Nenhum dos dois é um salto gigante sobre o 0.807 da floresta sozinha, mas os dois batem, de forma consistente, a versão mais simples. Diferente do post anterior (onde um modelo simples bateu todo ensemble tentado), aqui a complexidade extra realmente ajudou, um pouco. A lição de antes continua valendo: não dá pra saber qual vai ganhar sem medir os dois com a mesma régua.

Fechando

O que eu já sabiaO que essa aula assentou
Todo dataset até aqui já vinha completoDado faltando é normal em dado real, e a solução (imputação) precisa de uma estratégia pensada, não só "descartar a linha"
Categoria vira número de algum jeitoOne-hot encoding evita inventar uma ordem que não existe entre categorias
Validação cruzada é mais honesta que medir no treinoMesmo alerta de sempre, mas dessa vez numa Random Forest real: 98% no treino contra 81% de verdade

Aplicação Prática

Reproduzi o pipeline inteiro com semente fixa (o notebook original não fixa nenhuma), pra ter um número estável e poder comparar as três abordagens sob o mesmo critério exato de validação cruzada.

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores_rf = cross_val_score(clf_rf, X, y, cv=cv)
scores_stacking = cross_val_score(clf_stacking, X, y, cv=cv)
scores_mlp = cross_val_score(clf_mlp, X, y, cv=cv)
ModeloAcurácia (treino, decoreba)Acurácia (validação cruzada)Desvio padrão
Random Forest0.97980.80920.0268
Stacking (8 modelos)(não medi)0.82830.0077
Rede neural (MLP)(não medi)0.81820.0197

Os números batem de perto com os do notebook original (0.807, 0.824, 0.822), confirmando que a diferença entre os três não foi sorte de uma rodada específica. E repara no desvio padrão: o stacking não é só o que teve a melhor média, é também o mais estável de dobra pra dobra (0.0077 contra 0.0268 da floresta sozinha), sinal de que combinar vários modelos diferentes amorteceu parte da variação que cada um sozinho carrega.