Pular para conteúdo

14. Regressão linear

A interpolação da Unidade 5 fazia uma curva passar exatamente por todos os pontos. Isso faz sentido quando os pontos são exatos: um catálogo, uma tabela calculada. Com medições, não: cada medição traz um pouco de ruído, e uma curva obrigada a passar por todas elas passa também pelo ruído, oscilando sem motivo (capítulo 11).

A regressão faz outra pergunta: qual a curva mais simples que passa perto de todos os pontos, errando o mínimo possível? Com uma reta, é a regressão linear, a ferramenta mais usada de toda a análise de dados. E, surpresa, ela se resolve com um sistema linear 2 × 2, da Unidade 4.

Dados com ruído

🌍 Contexto: a latência de um servidor

Quanto mais usuários acessam um site ao mesmo tempo, mais demora cada resposta: é a latência, em milissegundos. Uma equipe de infraestrutura mede a latência média em várias cargas para saber quantos usuários o servidor aguenta antes de ficar lento. Cada medição varia um pouco (o que mais estava rodando, a rede naquele instante).

# Latencia de um servidor web (ms) x usuarios simultaneos. As medicoes tem
# ruido: passar uma curva EXATAMENTE por todas elas faz sentido?
import numpy as np
import matplotlib.pyplot as plt

dados = np.loadtxt("latencia_servidor.csv", delimiter=",", skiprows=1)
usuarios = dados[:, 0]
latencia = dados[:, 1]
print(f"{len(usuarios)} medicoes, de {usuarios[0]:.0f} a {usuarios[-1]:.0f} usuarios")

plt.figure()
plt.plot(usuarios, latencia, "o")
plt.xlabel("usuários simultâneos")
plt.ylabel("latência média (ms)")
plt.grid()
plt.show()
25 medicoes, de 20 a 500 usuarios

25 pontos que sobem mais ou menos em linha reta, espalhados em volta dela

Os pontos seguem uma reta, mas não estão em cima dela. Um polinômio de grau 24 passando por todos seria absurdo. O que se quer é a reta que melhor os representa.

No quadro: mínimos quadrados

🧑‍🏫 No quadro — a reta de mínimos quadrados

1. Para uma reta \(\hat y = a_0 + a_1 x\), o resíduo de cada ponto é o quanto a reta erra ali: \(e_i = y_i - (a_0 + a_1 x_i)\).

2. Uns resíduos são positivos, outros negativos, e somá-los não serve (eles se cancelam). Some os quadrados:

\[ S(a_0, a_1) = \sum_i \big(y_i - a_0 - a_1 x_i\big)^2. \]

A melhor reta é a que dá o menor \(S\): daí o nome, mínimos quadrados.

3. No mínimo, as duas derivadas parciais valem zero (como no capítulo 6, agora com duas variáveis):

\[ \frac{\partial S}{\partial a_0} = -2\sum_i (y_i - a_0 - a_1 x_i) = 0, \qquad \frac{\partial S}{\partial a_1} = -2\sum_i x_i\,(y_i - a_0 - a_1 x_i) = 0. \]

4. Rearrumando, saem as equações normais, um sistema linear 2 × 2:

\[ \begin{bmatrix} n & \sum x_i \\ \sum x_i & \sum x_i^2 \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \end{bmatrix} = \begin{bmatrix} \sum y_i \\ \sum x_i y_i \end{bmatrix}. \]

5. Os ingredientes são quatro somas; a resolução é a do capítulo 7.

# A reta que erra menos: minimos quadrados. As duas equacoes normais formam
# um sistema linear 2 x 2 nos coeficientes a0 (intercepto) e a1 (inclinacao).
import numpy as np
import matplotlib.pyplot as plt

dados = np.loadtxt("latencia_servidor.csv", delimiter=",", skiprows=1)
x = dados[:, 0]
y = dados[:, 1]
n = len(x)

A = np.array([[n, np.sum(x)],
              [np.sum(x), np.sum(x**2)]])
b = np.array([np.sum(y), np.sum(x * y)])
a0, a1 = np.linalg.solve(A, b)
print(f"reta: latencia = {a0:.3f} + {a1:.5f} * usuarios")
print(f"previsao para 600 usuarios: {a0 + a1 * 600:.1f} ms")

plt.figure()
plt.plot(x, y, "o", label="medições")
plt.plot(x, a0 + a1 * x, label="reta de mínimos quadrados")
plt.xlabel("usuários simultâneos")
plt.ylabel("latência média (ms)")
plt.grid()
plt.legend()
plt.show()
reta: latencia = 12.765 + 0.07900 * usuarios
previsao para 600 usuarios: 60.2 ms

A reta de mínimos quadrados passa pelo meio da nuvem de pontos

A reta diz que o servidor tem uma latência de base de cerca de 12,8 ms e que cada usuário a mais acrescenta 0,079 ms. Com ela, a equipe prevê a latência com 600 usuários (60 ms), numa carga que ainda não foi testada, mas próxima das testadas.

A linha a0, a1 = np.linalg.solve(A, b) recebe os dois números da solução em dois nomes de uma vez, como o valor, erro = quad(...) do capítulo 12.

Quão boa é a reta

Uma reta sempre pode ser calculada, mesmo para pontos que não têm nada de reta. Duas medidas dizem se ela serve. Elas usam a média, que o numpy também calcula.

🧰 Comando novo: np.mean

np.mean(a) é a média dos elementos: a soma dividida pela quantidade.

import numpy as np

notas = np.array([6.5, 8.0, 7.5, 9.0])
print(np.mean(notas))              # a media: a soma dividida pela quantidade
print(np.sum(notas) / len(notas))  # a mesma conta, a mao
7.75
7.75

O coeficiente de determinação \(R^2\) compara o erro da reta com o erro de "chutar a média" para todo mundo:

\[ R^2 = 1 - \frac{\sum_i (y_i - \hat y_i)^2}{\sum_i (y_i - \bar y)^2}. \]

\(R^2 = 1\) quer dizer que a reta passa por todos os pontos; \(R^2 = 0\), que ela não explica nada além da média.

# Quao boa e a reta? O coeficiente de determinacao R²: a fracao da variacao
# de y que a reta explica. 1 = perfeita; 0 = a reta nao explica nada.
import numpy as np

dados = np.loadtxt("latencia_servidor.csv", delimiter=",", skiprows=1)
x = dados[:, 0]
y = dados[:, 1]
n = len(x)
A = np.array([[n, np.sum(x)], [np.sum(x), np.sum(x**2)]])
b = np.array([np.sum(y), np.sum(x * y)])
a0, a1 = np.linalg.solve(A, b)

previsto = a0 + a1 * x
residuos = y - previsto
sq_residuos = np.sum(residuos**2)            # o que a reta NAO explica
sq_total = np.sum((y - np.mean(y))**2)       # a variacao total em torno da media
r2 = 1 - sq_residuos / sq_total
print(f"R² = {r2:.4f}")
print(f"desvio tipico dos residuos: {np.sqrt(sq_residuos / (n - 2)):.2f} ms")
R² = 0.9532
desvio tipico dos residuos: 2.63 ms

A reta explica 95 % da variação da latência. O resto (desvio típico de 2,6 ms) é o ruído das medições.

Os resíduos contam o resto da história

Um \(R^2\) alto não garante que a reta seja o modelo certo. O teste de verdade é olhar os resíduos: se a reta está certa, eles são ruído, espalhados sem padrão em volta de zero. Se há um padrão, a reta está deixando algo de fora.

🌍 Contexto: o CO₂ de Mauna Loa

Desde 1958, o observatório de Mauna Loa, no Havaí, mede a concentração de gás carbônico no ar, longe de cidades e indústrias. A série, em partes por milhão (ppm), é o registro mais famoso do aumento do CO₂ na atmosfera.

# Os residuos contam o que a reta deixou de fora. CO2 na atmosfera (Mauna Loa):
# uma reta tem R² alto -- mas olhe os residuos.
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]
n = len(ano)
A = np.array([[n, np.sum(ano)], [np.sum(ano), np.sum(ano**2)]])
b = np.array([np.sum(co2), np.sum(ano * co2)])
a0, a1 = np.linalg.solve(A, b)
residuos = co2 - (a0 + a1 * ano)
r2 = 1 - np.sum(residuos**2) / np.sum((co2 - np.mean(co2))**2)
print(f"reta: {a1:.3f} ppm por ano, R² = {r2:.4f}")
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 reta: um U, e não um ruído")
plt.grid()
plt.show()
reta: 1.614 ppm por ano, R² = 0.9815
residuos: [ 7.7  2.8  0.4 -2.3 -2.7 -3.4 -3.2 -4.8 -4.  -2.   0.1  3.1  8.3]

Os resíduos da reta formam um U: positivos nas pontas, negativos no meio

💥 Quebre o método

\(R^2 = 0{,}98\), e a reta está errada. Os resíduos formam um U perfeito: positivos no começo, negativos no meio, positivos no fim. É a marca de uma curva que acelera: o CO₂ não cresce a um ritmo constante, cresce cada vez mais depressa. A reta subestima o presente e, pior, subestimaria o futuro. Sempre olhe o gráfico dos resíduos. O próximo capítulo ajusta uma curva.

Correlação não é causalidade

Uma reta bem ajustada diz que \(x\) e \(y\) andam juntos, e não que um causa o outro. As vendas de sorvete e os afogamentos em praias sobem juntos no verão; nenhum causa o outro, os dois são causados pelo calor. Numa regressão, a pergunta "por quê?" continua sendo trabalho de quem conhece o problema.

Confira com a biblioteca

🧰 Comando novo: np.polyfit

np.polyfit(x, y, g) devolve os coeficientes do polinômio de grau g que melhor se ajusta aos pontos, por mínimos quadrados. Atenção à ordem: o coeficiente do grau mais alto vem primeiro. Para uma reta, np.polyfit(x, y, 1) devolve [inclinação, intercepto], o contrário da ordem \(a_0, a_1\) das equações normais.

# Confira com a biblioteca.
import numpy as np

dados = np.loadtxt("latencia_servidor.csv", delimiter=",", skiprows=1)
x = dados[:, 0]
y = dados[:, 1]
coef = np.polyfit(x, y, 1)      # grau 1: uma reta
print("np.polyfit:", coef)       # ATENCAO: [inclinacao, intercepto] -- o grau mais alto primeiro
np.polyfit: [ 0.07899615 12.765     ]

Mesmo método, outra área

🌍 Contexto: o recorde dos 100 metros

Desde que a cronometragem passou a ser eletrônica (1968), o recorde mundial dos 100 m rasos masculino caiu de 9,95 s (Jim Hines) para 9,58 s (Usain Bolt, 2009).

# Mesmo metodo, outra area: o recorde mundial dos 100 m rasos, de 1968 a 2009.
import numpy as np

dados = np.loadtxt("recordes_100m.csv", delimiter=",", skiprows=1)
ano = dados[:, 0]
tempo = dados[:, 1]
coef = np.polyfit(ano, tempo, 1)
a1 = coef[0]
a0 = coef[1]
print(f"o recorde cai {-a1 * 100:.2f} segundos por seculo")
print(f"previsao para 2030: {a0 + a1 * 2030:.2f} s")
print(f"ano em que o recorde seria 0 s: {-a0 / a1:.0f}")
o recorde cai 0.80 segundos por seculo
previsao para 2030: 9.54 s
ano em que o recorde seria 0 s: 3229

A reta diz que o recorde cai 0,8 s por século e prevê 9,54 s em 2030, uma previsão razoável, perto dos dados. A mesma reta diz que, no ano 3229, os 100 m seriam corridos em zero segundos. É a extrapolação do capítulo 10 de novo: fora da faixa dos dados, o modelo não sabe nada. Há um limite físico que a reta ignora.

Erros comuns deste capítulo

Sintoma Causa provável
inclinação e intercepto trocados np.polyfit devolve do grau mais alto para o mais baixo: [a1, a0]
\(R^2\) negativo a reta foi calculada errada (ou com outros dados): mínimos quadrados sempre dá \(R^2 \geq 0\)
reta boa, previsões ruins extrapolação: previsão longe da faixa dos dados
\(R^2\) alto, mas o modelo está errado resíduos com padrão (curva, crescimento do espalhamento): olhe o gráfico dos resíduos
concluir que \(x\) causa \(y\) correlação não é causalidade

Onde isso é usado de verdade

  • Engenharia e ciência: calibrar sensores (a reta que converte a leitura em grandeza física), estimar constantes físicas a partir de experimentos.
  • Computação e redes: planejar a capacidade de servidores e links (quanto a latência ou o tráfego cresce com os usuários).
  • Economia e negócios: tendência de vendas, custo em função da produção.
  • Saúde: curvas de referência (peso por altura), dose por peso corporal.
  • Aprendizado de máquina: a regressão linear é o primeiro modelo de todo curso de machine learning, e as redes neurais são, no fundo, regressões muito maiores.

Resumo

  • Com medições que têm ruído, não se interpola: ajusta-se a curva que erra menos.
  • Mínimos quadrados: minimizar a soma dos quadrados dos resíduos. Para a reta, isso dá as equações normais, um sistema 2 × 2 com quatro somas.
  • \(R^2\) mede quanto da variação a reta explica. Mas só o gráfico dos resíduos diz se o modelo está certo.
  • np.polyfit(x, y, 1) confere, devolvendo [inclinação, intercepto].
  • Correlação não é causalidade, e extrapolação é perigosa.

Para praticar

A Lista 14 começa com uma regressão à mão (✏️) e segue com o \(R^2\), a calibração de um sensor, a correlação e a previsão com e sem extrapolação.