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:
Agefalta em 177 dos 891 passageiros (quase 20%).Embarkedfalta 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á sabia | O que essa aula assentou |
|---|---|
| Todo dataset até aqui já vinha completo | Dado 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 jeito | One-hot encoding evita inventar uma ordem que não existe entre categorias |
| Validação cruzada é mais honesta que medir no treino | Mesmo 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)
| Modelo | Acurácia (treino, decoreba) | Acurácia (validação cruzada) | Desvio padrão |
|---|---|---|---|
| Random Forest | 0.9798 | 0.8092 | 0.0268 |
| Stacking (8 modelos) | (não medi) | 0.8283 | 0.0077 |
| Rede neural (MLP) | (não medi) | 0.8182 | 0.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.