Pular para conteúdo

15. Ajuste não linear

No capítulo 14, os resíduos do CO₂ formaram um U: a reta não bastava, porque o CO₂ acelera. Muitos fenômenos não seguem retas: uma amostra radioativa decai exponencialmente, uma epidemia cresce e freia numa curva em S, o sinal de rádio cai com o logaritmo da distância, o metabolismo dos animais cresce com uma potência da massa.

Este capítulo mostra três saídas, da mais simples para a mais geral: um polinômio (a mesma conta da reta, com mais termos), a linearização (transformar os dados até a relação virar reta) e o ajuste direto de qualquer modelo, com uma ferramenta pronta. E mostra também o perigo do outro lado: um modelo flexível demais, que passa a ajustar o ruído.

Quando a reta não basta: o polinômio

A regressão linear minimiza a soma dos quadrados dos resíduos. Nada impede que o modelo tenha mais termos: \(\hat y = a_0 + a_1 t + a_2 t^2\). As equações normais continuam sendo um sistema linear (agora 3 × 3), porque o modelo é linear nos coeficientes, mesmo sendo uma curva em \(t\). É o que o np.polyfit faz com grau 2.

# O CO2 acelera: uma parabola, e nao uma reta. Os anos sao contados a partir
# de 1960 (t = ano - 1960) para os numeros da matriz nao ficarem enormes.
import numpy as np
import matplotlib.pyplot as plt

dados = np.loadtxt("co2_mauna_loa.csv", delimiter=",", skiprows=1)
ano = dados[:, 0]
co2 = dados[:, 1]
t = ano - 1960

coef = np.polyfit(t, co2, 2)            # [a2, a1, a0]
previsto = coef[0] * t**2 + coef[1] * t + coef[2]
residuos = co2 - previsto
r2 = 1 - np.sum(residuos**2) / np.sum((co2 - np.mean(co2))**2)
print(f"CO2 = {coef[2]:.2f} + {coef[1]:.4f} t + {coef[0]:.5f} t²    (t = ano - 1960)")
print(f"R² = {r2:.5f}")
print("residuos:", np.round(residuos, 1))

plt.figure()
plt.plot(ano, residuos, "o-")
plt.axhline(0, color="black")
plt.xlabel("ano")
plt.ylabel("resíduo (ppm)")
plt.title("Resíduos da parábola: agora sem padrão")
plt.grid()
plt.show()
CO2 = 316.39 + 0.8259 t + 0.01313 t²    (t = ano - 1960)
R² = 0.99941
residuos: [ 0.5 -0.8 -0.3 -0.6  0.6  0.9  1.4 -0.6 -0.7 -0.3 -0.6 -0.5  1. ]

Os resíduos da parábola oscilam sem padrão em torno de zero

O \(R^2\) subiu de 0,98 para 0,9994, mas o que importa é o gráfico: os resíduos agora oscilam sem padrão, entre −1 e 1,4 ppm. O termo \(0{,}013\,t^2\) é a aceleração que a reta não via.

💡 Pense assim: por que \(t = \text{ano} - 1960\)?

Com o ano direto, \(x^2\) vale quase 4 milhões e a matriz das equações normais mistura números de ordens de grandeza muito diferentes: fica mal condicionada (capítulo 7). Contar o tempo a partir do começo dos dados deixa os números pequenos e a conta, precisa.

Sobreajuste

Se o grau 2 foi melhor que o 1, grau 10 seria ainda melhor? O teste certo é separar os dados: uma parte ajusta o modelo (treino) e a outra, que ele não viu, confere (teste).

# Quebre o metodo: polinomios de grau cada vez maior nos dados da latencia.
# Metade dos pontos ajusta (treino); a outra metade confere (teste).
import numpy as np

dados = np.loadtxt("latencia_servidor.csv", delimiter=",", skiprows=1)
x = dados[:, 0] / 100            # centenas de usuarios (numeros menores)
y = dados[:, 1]
x_treino = x[0::2]               # posicoes pares
y_treino = y[0::2]
x_teste = x[1::2]                # posicoes impares
y_teste = y[1::2]


def avalia(coef, x):
    grau = len(coef) - 1
    total = 0.0
    for k in range(len(coef)):
        total = total + coef[k] * x**(grau - k)
    return total


print("grau  erro no treino  erro no teste   (desvio tipico, ms)")
for grau in [1, 2, 4, 6, 8, 10]:
    coef = np.polyfit(x_treino, y_treino, grau)
    erro_treino = np.sqrt(np.mean((avalia(coef, x_treino) - y_treino)**2))
    erro_teste = np.sqrt(np.mean((avalia(coef, x_teste) - y_teste)**2))
    print(f"{grau:4d}   {erro_treino:12.2f}   {erro_teste:12.2f}")
grau  erro no treino  erro no teste   (desvio tipico, ms)
   1           2.66           2.39
   2           2.66           2.42
   4           1.88           2.37
   6           1.61           2.67
   8           0.92           2.84
  10           0.74           4.03

💥 Quebre o método

Com o grau subindo, o erro no treino só cai: um polinômio de grau 10 tem coeficientes de sobra para se contorcer e passar perto de cada ponto, ruído incluído. Mas o erro no teste sobe de 2,4 para 4,0 ms: o modelo decorou o ruído do treino e erra mais nos pontos novos. É o sobreajuste (overfitting), o fenômeno de Runge do capítulo 11 vestido de estatística. A regra: escolha o modelo pelo erro no teste, e prefira o mais simples que explica os dados.

No quadro: a linearização

Alguns modelos curvos viram retas com uma troca de variável. Aí, a reta de mínimos quadrados do capítulo 14 resolve tudo.

🧑‍🏫 No quadro — linearizando exponenciais e potências

1. Exponencial, \(y = a\,e^{bx}\) (crescimento de bactérias, decaimento radioativo, descarga de um capacitor). Tire o logaritmo natural dos dois lados:

\[ \ln y = \ln a + b\,x. \]

É uma reta entre \(x\) e \(\ln y\), com inclinação \(b\) e intercepto \(\ln a\).

2. Potência, \(y = a\,x^b\) (lei de Kleiber, área × lado, resistência do ar × velocidade). O logaritmo dos dois lados:

\[ \ln y = \ln a + b\,\ln x. \]

É uma reta entre \(\ln x\) e \(\ln y\): o gráfico log-log do capítulo 2.

3. Ajuste a reta nos dados transformados e desfaça a transformação: \(a = e^{\text{intercepto}}\).

4. Um detalhe honesto: minimizar os quadrados em \(\ln y\) não é o mesmo que em \(y\) (os pontos pequenos ganham mais peso). Quase sempre é bom o bastante; quando não for, use o ajuste direto da próxima seção.

🌍 Contexto: meia-vida

Uma amostra radioativa perde, a cada instante, uma fração fixa dos seus átomos: o número deles decai como \(N_0\,e^{-kt}\). A meia-vida, o tempo para cair à metade, é \(\ln 2 / k\). O iodo-131, usado no diagnóstico e tratamento da tireoide, tem meia-vida de 8 dias. Um detector conta os decaimentos da amostra a cada dia.

# Medicina nuclear: um detector conta os decaimentos de uma amostra de iodo-131
# ao longo dos dias. N = N0 * e^(-k t). No logaritmo, ln N = ln N0 - k t: uma
# reta em t -- e a reta ja sabemos ajustar.
import numpy as np
import matplotlib.pyplot as plt

dia = np.array([0, 2, 4, 6, 8, 10, 12, 14, 16])
contagem = np.array([10020, 8440, 7010, 5950, 4970, 4230, 3500, 2980, 2510])

coef = np.polyfit(dia, np.log(contagem), 1)     # a reta de ln(contagem)
k = -coef[0]
N0 = np.exp(coef[1])
meia_vida = np.log(2) / k
print(f"N0 = {N0:.0f} contagens, k = {k:.4f} por dia")
print(f"meia-vida = {meia_vida:.2f} dias   (a do iodo-131 e 8,02 dias)")

plt.figure()
plt.semilogy(dia, contagem, "o", label="medições")
plt.semilogy(dia, N0 * np.exp(-k * dia), label="ajuste exponencial")
plt.xlabel("dia")
plt.ylabel("contagens (escala log)")
plt.grid()
plt.legend()
plt.show()
N0 = 9989 contagens, k = 0.0866 por dia
meia-vida = 8.00 dias   (a do iodo-131 e 8,02 dias)

Em escala log, as contagens caem numa linha reta, e o ajuste passa por elas

A meia-vida medida, 8,00 dias, bate com a do iodo-131. No gráfico com o eixo vertical em escala log (plt.semilogy, do capítulo 4), a exponencial vira reta, e a conferência é visual.

Em telecomunicações, as potências são medidas em decibéis, que usam o logaritmo na base 10.

🧰 Comando novo: np.log10

np.log10(x) é o logaritmo na base 10: np.log10(1000) é 3, porque \(10^3 = 1000\). É o logaritmo dos decibéis: \(10\log_{10}(P_1/P_2)\). Dá o mesmo que np.log(x) / np.log(10).

import numpy as np

print(np.log10(1000))     # 10 elevado a 3 da 1000
print(np.log10(2))        # 10 elevado a 0.301 da 2
print(np.log(1000) / np.log(10))   # a mesma conta com o log natural
3.0
0.3010299956639812
2.9999999999999996

Um modelo que não se lineariza

A curva logística de uma epidemia, \(C(t) = \dfrac{K}{1 + A\,e^{-rt}}\), não vira reta com nenhum logaritmo. Para ela, e para qualquer modelo, existe o ajuste direto: um método iterativo que procura os parâmetros que minimizam a soma dos quadrados (parente do Newton para otimização do capítulo 6, com várias variáveis).

🧰 Comando novo: from scipy.optimize import curve_fit

curve_fit(modelo, x, y, p0=[...]) ajusta os parâmetros de uma função modelo(x, p1, p2, ...) aos dados. O primeiro argumento do modelo é sempre o \(x\); os outros são os parâmetros a ajustar. p0= é a lista dos chutes iniciais, um por parâmetro: como no Newton, um chute ruim pode levar a uma resposta ruim. Ele devolve dois resultados: os parâmetros ajustados e uma matriz com a incerteza deles (que o curso não usa).

# Um modelo que nao se lineariza: a curva logistica de uma epidemia,
#   C(t) = K / (1 + A e^(-r t)).
# O curve_fit do scipy ajusta os tres parametros direto, por minimos quadrados.
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

dados = np.loadtxt("casos_acumulados.csv", delimiter=",", skiprows=1)
dia = dados[:, 0]
casos = dados[:, 1]


def logistica(t, K, A, r):
    return K / (1 + A * np.exp(-r * t))


parametros, covariancia = curve_fit(logistica, dia, casos, p0=[10000, 100, 0.1])
K, A, r = parametros
print(f"K = {K:.0f} casos no total, A = {A:.1f}, r = {r:.4f} por dia")
print(f"pico de casos novos (ponto de inflexao) no dia {np.log(A) / r:.1f}")

plt.figure()
plt.plot(dia, casos, ".", label="casos acumulados")
plt.plot(dia, logistica(dia, K, A, r), label="logística ajustada")
plt.xlabel("dia")
plt.ylabel("casos")
plt.grid()
plt.legend()
plt.show()
K = 11884 casos no total, A = 372.0, r = 0.1188 por dia
pico de casos novos (ponto de inflexao) no dia 49.8

A curva logística ajustada passa pelo meio dos casos acumulados

Com os dados de casos acumulados do capítulo 3, o ajuste estima o total da epidemia (cerca de 11 900 casos) e o dia do pico (49,8). No capítulo 3, a derivada do dado com ruído dizia dia 47; o modelo do qual os dados foram gerados diz 49,9. Ajustar um modelo às medições filtra o ruído que a derivada amplificava.

Mesmo método, outra área

🌍 Contexto: a lei de Kleiber

Em 1932, o biólogo Max Kleiber notou que o metabolismo basal dos animais (a energia gasta em repouso) cresce com a massa elevada a um expoente perto de 3/4, do camundongo ao elefante. Um animal 10 vezes mais pesado não gasta 10 vezes mais energia, mas cerca de 5,6. É uma das leis mais gerais da biologia, e ainda se discute por que o expoente é esse.

# Mesmo metodo, outra area: a lei de Kleiber. O gasto de energia de um animal em
# repouso (metabolismo basal) cresce com a massa, mas nao em proporcao direta:
# M = a * m^b. No log-log, ln M = ln a + b ln m: uma reta.
# (valores aproximados, de livros de fisiologia)
import numpy as np

animal = ["camundongo", "rato", "gato", "cachorro", "humano", "cavalo", "elefante"]
massa = np.array([0.021, 0.29, 3.0, 15.0, 70.0, 500.0, 4000.0])        # kg
metabolismo = np.array([0.21, 1.45, 8.3, 30.0, 80.0, 350.0, 2300.0])    # W

coef = np.polyfit(np.log(massa), np.log(metabolismo), 1)
b = coef[0]
a = np.exp(coef[1])
print(f"M = {a:.2f} * m^{b:.3f}")
print(f"um animal 10 vezes mais pesado gasta {10**b:.1f} vezes mais energia")
M = 3.68 * m^0.755
um animal 10 vezes mais pesado gasta 5.7 vezes mais energia

A reta no gráfico log-log dá o expoente 0,755: a lei de Kleiber, a partir de sete animais e de um np.polyfit nos logaritmos.

Erros comuns deste capítulo

Sintoma Causa provável
nan ou erro no np.log logaritmo de zero ou de número negativo: a linearização exige \(y > 0\)
o \(a\) ajustado sai estranho faltou desfazer o logaritmo: \(a = e^{\text{intercepto}}\)
curve_fit avisa que não convergiu, ou dá parâmetros absurdos chutes iniciais (p0) ruins: comece com valores de ordem de grandeza certa, olhando o gráfico
polinômio de grau alto com coeficientes estranhos números grandes em \(x\) (anos): conte a partir do começo dos dados
o modelo ajusta o treino e erra o resto sobreajuste: reduza o grau, confira com dados de teste

Onde isso é usado de verdade

  • Telecomunicações: os modelos de propagação (quanto o sinal cai com a distância) são ajustados a medições de campo, em dB e escala log. É assim que as operadoras planejam onde pôr antenas.
  • Saúde: meia-vida de remédios, curvas de epidemia, curvas dose-resposta.
  • Física e química: constantes de decaimento, de reação, de resfriamento, tudo por ajuste de exponenciais.
  • Aprendizado de máquina: treino e teste, sobreajuste e a escolha da complexidade do modelo são o dia a dia da área.
  • Biologia e ecologia: leis de potência (Kleiber, área × número de espécies), crescimento populacional.

Resumo

  • Um polinômio de grau maior ainda é ajuste linear nos coeficientes: np.polyfit com grau 2 ou 3.
  • Mais parâmetros podem sobreajustar: confira o modelo com dados de teste que ele não viu.
  • Linearização: exponencial vira reta em \((x, \ln y)\); potência, em \((\ln x, \ln y)\).
  • Modelos que não se linearizam se ajustam direto, com curve_fit e bons chutes iniciais.
  • Sempre confira os resíduos: eles dizem se o modelo capturou o fenômeno.

Para praticar

A Lista 15 tem uma linearização à mão (✏️), o ajuste de uma exponencial e de uma potência, a escolha do grau por treino e teste e o ajuste de uma curva de aprendizagem.