#8 - Escala de Dados e Divisão Treino/Teste

Como padronizar colunas em escalas diferentes com StandardScaler, e como dividir um dataset em treino e teste pra avaliar se um modelo generaliza bem.

#8 - Escala de Dados e Divisão Treino/Teste

Dois problemas bem práticos aparecem antes de qualquer regressão múltipla séria: como comparar colunas que estão em unidades completamente diferentes, e como saber se um modelo realmente aprendeu alguma coisa, em vez de simplesmente decorar os dados que viu.

Escalando dados

Quando os dados têm valores — e até unidades — bem diferentes entre si, fica difícil compará-los. O que é 1000 quilos comparado a 1.6 litro? É pra isso que serve o escalonamento (scaling): transformar os dados em novos valores mais fáceis de comparar entre si.

Vamos usar o mesmo dataset de carros do post anterior, mas com o Volume do motor agora em litros (1.0) em vez de cm³ (1000). O método usado aqui se chama padronização (standardization), com a fórmula:

z = (x - u) / s

Onde z é o novo valor, x é o valor original, u é a média, e s é o desvio padrão.

Pra coluna Weight, o primeiro valor é 790; a média é 1292.23 e o desvio padrão é 238.74: (790 - 1292.23) / 238.74 = -2.1. Pra coluna Volume, o primeiro valor é 1.0; a média é 1.61 e o desvio padrão é 0.38: (1.0 - 1.61) / 0.38 = -1.59. Agora dá pra comparar -2.1 com -1.59, em vez de comparar 790 com 1.0.

Não é preciso fazer essa conta na mão — o StandardScaler do sklearn já faz isso:

import pandas
from sklearn.preprocessing import StandardScaler

scale = StandardScaler()

df = pandas.read_csv("data.csv")

X = df[['Weight', 'Volume']]

X_escalado = scale.fit_transform(X)

print(X_escalado)

A saída é:

[[-2.10389253 -1.59336644]
 [-0.55407235 -1.07190106]
 [-1.52166278 -1.59336644]
 [-1.78973979 -1.85409913]
 [-0.63784641 -0.28970299]
 [-1.52166278 -1.59336644]
 [-0.76769621 -0.55043568]
 [ 0.3046118  -0.28970299]
 [-0.7551301  -0.28970299]
 [-0.59595938 -0.0289703 ]
 [-1.30803892 -1.33263375]
 [-1.26615189 -0.81116837]
 [-0.7551301  -1.59336644]
 [-0.16871166 -0.0289703 ]
 [ 0.14125238 -0.0289703 ]
 [ 0.15800719 -0.0289703 ]
 [ 0.3046118  -0.0289703 ]
 [-0.05142797  1.53542584]
 [-0.72580918 -0.0289703 ]
 [ 0.14962979  1.01396046]
 [ 1.2219378  -0.0289703 ]
 [ 0.5685001   1.01396046]
 [ 0.3046118   1.27469315]
 [ 0.51404696 -0.0289703 ]
 [ 0.51404696  1.01396046]
 [ 0.72348212 -0.28970299]
 [ 0.8281997   1.01396046]
 [ 1.81254495  1.01396046]
 [ 0.96642691 -0.0289703 ]
 [ 1.72877089  1.01396046]
 [ 1.30990057  1.27469315]
 [ 1.90050772  1.01396046]
 [-0.23991961 -0.0289703 ]
 [ 0.40932938 -0.0289703 ]
 [ 0.47215993 -0.0289703 ]
 [ 0.4302729   2.31762392]]

Repare que os dois primeiros valores são -2.1 e -1.59 — exatamente os mesmos que calculamos manualmente.

Nota: ao prever um valor novo com dados escalados, é essencial escalar a nova entrada da mesma forma antes de passar pro modelo — usando scale.transform(), não fit_transform() de novo, senão a previsão usa uma escala diferente da que o modelo aprendeu.

import pandas
from sklearn import linear_model
from sklearn.preprocessing import StandardScaler

scale = StandardScaler()

df = pandas.read_csv("data.csv")

X = df[['Weight', 'Volume']]
y = df['CO2']

X_escalado = scale.fit_transform(X)

regr = linear_model.LinearRegression()
regr.fit(X_escalado, y)

escalado = scale.transform([[2300, 1.3]])

co2_previsto = regr.predict([escalado[0]])
print(co2_previsto)

A saída é:

[107.2087328]

O mesmo resultado do post anterior — ~107g de CO2 pra um carro de 2300kg com motor de 1.3 litro — confirmando que escalar os dados de entrada e de saída da mesma forma preserva a previsão.

Dividindo em treino e teste

Depois de treinar um modelo, como saber se ele é bom o suficiente? Um jeito comum é o método treino/teste: dividir o dataset em duas partes — normalmente 80% pra treino e 20% pra teste. Você treina o modelo com uma parte dos dados, e testa a precisão dele com a outra parte, que o modelo nunca viu.

O dataset deste exemplo simula 100 clientes de uma loja: x é o número de minutos que cada cliente passou na loja antes de comprar, y é o valor gasto.

import numpy
import matplotlib.pyplot as plt

numpy.random.seed(2)

x = numpy.random.normal(3, 1, 100)
y = numpy.random.normal(150, 40, 100) / x

plt.scatter(x, y)
plt.show()

Separando 80% pros dados de treino e os 20% restantes pro teste:

treino_x = x[:80]
treino_y = y[:80]

teste_x = x[80:]
teste_y = y[80:]

Plotando cada conjunto separadamente (plt.scatter(treino_x, treino_y) e depois plt.scatter(teste_x, teste_y)), os dois parecem manter a mesma forma do dataset original — um bom sinal de que a divisão foi justa, sem viés.

Ajustando o modelo

Olhando pro formato da curva, uma regressão polinomial de grau 4 parece ser a melhor opção aqui:

import numpy
import matplotlib.pyplot as plt

numpy.random.seed(2)

x = numpy.random.normal(3, 1, 100)
y = numpy.random.normal(150, 40, 100) / x

treino_x = x[:80]
treino_y = y[:80]

meu_modelo = numpy.poly1d(numpy.polyfit(treino_x, treino_y, 4))

minha_linha = numpy.linspace(0, 6, 100)

plt.scatter(treino_x, treino_y)
plt.plot(minha_linha, meu_modelo(minha_linha))
plt.show()

Nota: fora do intervalo do dataset, essa curva dá resultados estranhos — por exemplo, ela sugere que um cliente que passasse 6 minutos na loja gastaria 200. Isso é sinal de overfitting: o modelo se ajustou demais aos dados de treino, a ponto de “inventar” padrões que não existem de verdade fora deles.

Medindo com R²

import numpy
from sklearn.metrics import r2_score

numpy.random.seed(2)

x = numpy.random.normal(3, 1, 100)
y = numpy.random.normal(150, 40, 100) / x

treino_x = x[:80]
treino_y = y[:80]

meu_modelo = numpy.poly1d(numpy.polyfit(treino_x, treino_y, 4))

r2 = r2_score(treino_y, meu_modelo(treino_x))

print(r2)

A saída é:

0.799

Um R² de 0.799 no treino mostra uma relação razoável. Mas o teste de verdade é ver se o modelo se sai bem também nos dados de teste, que ele nunca viu durante o treino:

import numpy
from sklearn.metrics import r2_score

numpy.random.seed(2)

x = numpy.random.normal(3, 1, 100)
y = numpy.random.normal(150, 40, 100) / x

treino_x = x[:80]
treino_y = y[:80]

teste_x = x[80:]
teste_y = y[80:]

meu_modelo = numpy.poly1d(numpy.polyfit(treino_x, treino_y, 4))

r2 = r2_score(teste_y, meu_modelo(teste_x))

print(r2)

A saída é:

0.809

O R² do teste (0.809) fica bem próximo do R² do treino (0.799) — o modelo se comporta de forma parecida em dados que nunca viu, o que dá confiança pra usá-lo em previsões novas.

Prevendo um valor novo

Quanto um cliente vai gastar se passar 5 minutos na loja?

print(meu_modelo(5))

A saída é:

22.88

O modelo prevê que um cliente gastaria cerca de 22.88 — um valor que faz sentido olhando pro formato geral do gráfico.

Com dados escalados e devidamente separados em treino e teste, você tem as ferramentas pra confiar (ou desconfiar) de um modelo antes de usá-lo de verdade. No próximo post, você conhece um tipo de modelo bem diferente das regressões vistas até aqui: a árvore de decisão.

Fonte adaptada: Scale, Train/Test