Os materiais anteriores da série regressao-polinomial-eolico
a regressao-partes23-eolico, e
o aprofundamento em regressao-partes-texto
construíram uma sequencia completa: médias, retas, parábolas e cúbicas
por partes, utilizando o conjunto de dados eólicos e com
dado simulado de nó conhecido que foi utilizado para validar estimação e
bootstrap. Outros tópicos ficaram visíveis com dados que
sugeriam uma curvatura mais pronunciada e extrapolar para fora do
domínio observado, ilustrado nos scripts
medias2splines.R e xySpline.R exploram.
Este material explora o conteúdo desses dois scripts e trata:
splines::bs(): nós explícitos
vs. automáticos (df=), e o papel de
Boundary.knots;ns()): a
restrição extra que impõem, o custo em ajuste, e por que compensa fora
da amostra;splines::bs() e
splines::ns() são comentadas;Dados simulados com curvatura real, bem diferente da curva eólica,
quase linear, gerados com \(n=50\),
\(x\sim U(0,10)\) e \(Y_i = 0{,}2x_i + \cos(x_i+1) +
\varepsilon_i\), \(\varepsilon_i\sim
N(0,0{,}5^2)\), a mesma função geradora utilizada em
medias2splines.R.
library(splines) ## bs(), ns() -- carregado aqui (não em um chunk no meio do
## documento) porque o efeito colateral de attach do pacote
## não é reproduzido de forma confiável por chunks em cache
set.seed(201802)
df <- data.frame(x = sort(runif(50, 0, 10)))
df <- transform(df, Ytrue = 0.2 * x + cos(x + 1))
df <- transform(df, Y = Ytrue + rnorm(50, mean = 0, sd = 0.5))
nd <- data.frame(x = seq(0, 10, length = 2001))
nd <- transform(nd, Ytrue = 0.2 * x + cos(x + 1))
## NA exatamente nos nós, para não conectar segmentos separados nos gráficos abaixo
nd$x[nd$x == 3.5] <- NA
nd$x[nd$x == 7] <- NA
with(df, plot(Y ~ x, pch = 20, col = "gray30"))
with(nd, lines(Ytrue ~ x, lty = 3))
Dados simulados (curva verdadeira tracejada)
fitFun <- function(m) c(logLik = as.numeric(logLik(m)), df = attr(logLik(m), "df"),
AIC = AIC(m), BIC = BIC(m))
Com dois nós fixos em \(x=3{,}5\) e \(x=7\) (três regiões), a mesma escada de 0 a 3 restrições de regressao-partes23-eolico se aplica aqui. Não repetimos a dedução algébrica que já feita naquele material e nos concentramos no ajuste e a comparação, para servir de base à visualização de derivadas a seguir.
df <- transform(df, x1 = ifelse(x < 3.5, 0, x - 3.5), x2 = ifelse(x < 7, 0, x - 7))
nd <- transform(nd, x1 = ifelse(x < 3.5, 0, x - 3.5), x2 = ifelse(x < 7, 0, x - 7))
df <- transform(df, I1 = ifelse(x < 3.5, 0, 1), I2 = ifelse(x < 7, 0, 1))
nd <- transform(nd, I1 = ifelse(x < 3.5, 0, 1), I2 = ifelse(x < 7, 0, 1))
fit1 <- lm(Y ~ x, data = df) # reta global
fit3 <- lm(Y ~ poly(x, 3), data = df) # cúbica global
fit4 <- lm(Y ~ poly(x, 4), data = df) # quártica global
## 0 restrições ("livre"): jump permitido, cúbica independente em cada região
m3_livre <- lm(Y ~ poly(x, 3) + I1 + poly(x1, 3) + I2 + poly(x2, 3), data = df)
## 1 restrição (continuidade da função)
m3_r1 <- lm(Y ~ poly(x, 3) + poly(x1, 3) + poly(x2, 3), data = df)
## 2 restrições (continuidade da função e da 1a derivada)
m3_r2 <- lm(Y ~ poly(x, 3, raw = TRUE) + poly(x1, 3, raw = TRUE)[, -1] +
poly(x2, 3, raw = TRUE)[, -1], data = df)
## 3 restrições (spline cúbico "de fato")
m3_r3 <- lm(Y ~ poly(x, 3) + I(x1^3) + I(x2^3), data = df)
nd <- transform(nd, y1 = predict(fit1, newdata = nd), y3 = predict(fit3, newdata = nd),
y4 = predict(fit4, newdata = nd))
nd <- transform(nd, y3.livre = predict(m3_livre, newdata = nd),
y3.r1 = predict(m3_r1, newdata = nd),
y3.r2 = predict(m3_r2, newdata = nd),
y3.r3 = predict(m3_r3, newdata = nd))
sapply(list(livre = m3_livre, r1 = m3_r1, r2 = m3_r2, r3 = m3_r3, cubica = fit3, quartica = fit4), fitFun)
## livre r1 r2 r3 cubica quartica
## logLik -35.41825 -37.38099 -38.69683 -40.06912 -66.56633 -39.59515
## df 13.00000 11.00000 9.00000 7.00000 5.00000 6.00000
## AIC 96.83650 96.76198 95.39365 94.13824 143.13266 91.19029
## BIC 121.69280 117.79424 112.60186 107.52240 152.69278 102.66243
O painel a seguir é o coração da comparação: cada coluna mostra a curva ajustada (topo), sua 1ª derivada numérica (meio) e sua 2ª derivada numérica (base). As quebras que desaparecem de uma coluna para a seguinte são exatamente as restrições impostas — e ficam muito mais visíveis aqui do que na curva eólica (suave demais para revelar quinas de derivada), porque a curva geradora (\(\cos\)) tem curvatura de verdade.
dxx <- with(nd, unique(diff(x)))[1]
deriv1 <- function(y) c(diff(y) / dxx, NA)
deriv2 <- function(y1) c(diff(y1) / dxx, NA)
nd <- transform(nd, y3.livre.d1 = deriv1(y3.livre)); nd <- transform(nd, y3.livre.d2 = deriv2(y3.livre.d1))
nd <- transform(nd, y3.r1.d1 = deriv1(y3.r1)); nd <- transform(nd, y3.r1.d2 = deriv2(y3.r1.d1))
nd <- transform(nd, y3.r2.d1 = deriv1(y3.r2)); nd <- transform(nd, y3.r2.d2 = deriv2(y3.r2.d1))
nd <- transform(nd, y3.r3.d1 = deriv1(y3.r3)); nd <- transform(nd, y3.r3.d2 = deriv2(y3.r3.d1))
nd <- transform(nd, y3.d1 = deriv1(y3)); nd <- transform(nd, y3.d2 = deriv2(y3.d1))
nd <- transform(nd, y4.d1 = deriv1(y4)); nd <- transform(nd, y4.d2 = deriv2(y4.d1))
par(mfcol = c(3, 6), mar = c(3, 3, 1.5, 0.5), mgp = c(1.7, 0.7, 0))
for (nome in c("livre", "r1", "r2", "r3")) {
with(df, plot(Y ~ x, pch = 20, cex = 0.6, col = "gray40", main = nome))
with(nd, lines(get(paste0("y3.", nome)) ~ x))
plot(get(paste0("y3.", nome, ".d1")) ~ x, data = nd, type = "l", ylab = "1a deriv.")
plot(get(paste0("y3.", nome, ".d2")) ~ x, data = nd, type = "l", ylab = "2a deriv.")
}
with(df, plot(Y ~ x, pch = 20, cex = 0.6, col = "gray40", main = "cúbica global"))
with(nd, lines(y3 ~ x)); plot(y3.d1 ~ x, data = nd, type = "l", ylab = "1a deriv.")
plot(y3.d2 ~ x, data = nd, type = "l", ylab = "2a deriv.")
with(df, plot(Y ~ x, pch = 20, cex = 0.6, col = "gray40", main = "quártica global"))
with(nd, lines(y4 ~ x)); plot(y4.d1 ~ x, data = nd, type = "l", ylab = "1a deriv.")
plot(y4.d2 ~ x, data = nd, type = "l", ylab = "2a deriv.")
Função, 1ª e 2ª derivadas (numéricas): livre, 1, 2, 3 restrições, cúbica e quártica globais
par(mfrow = c(1, 1))
Note como a 1ª derivada só fica sem saltos a partir de “r2” (2
restrições) e a 2ª derivada só fica sem saltos em “r3” — e como as duas
últimas colunas (polinômios globais) não têm quinas em
lugar nenhum, mas em compensação não conseguem acompanhar a curvatura
local dos dados tão bem quanto a spline cúbica (compare os valores de
logLik na tabela acima: a cúbica global tem
log-verossimilhança pior que a spline “r3” com o mesmo número de
parâmetros).
Os quatro modelos acima foram construídos com potências de \(x\), \(x_1=(x-3{,}5)_+\) e \(x_2=(x-7)_+\) — a base de potências truncadas. Ela é a mais fácil de escrever à mão, mas tem um problema conhecido: colunas como \(x, x^2, x^3\) (e suas versões truncadas) ficam fortemente correlacionadas entre si, o que deixa \(X'X\) mal condicionada. Isso não afeta os valores ajustados (o modelo continua sendo exatamente o mesmo, matematicamente), mas afeta a precisão numérica do cálculo de \(\hat\beta=(X'X)^{-1}X'y\).
O número de condição de \(X'X\) mede esse mal-condicionamento — quanto maior, mais instável. Comparamos, para o mesmo modelo (mesmo nível de restrição, portanto mesmo espaço de colunas), duas parametrizações:
## Mesmo modelo (1 restrição), duas bases: potências cruas vs. poly() ortogonal
X_1r_raw <- model.matrix(Y ~ x + I(x^2) + I(x^3) +
I(x1) + I(x1^2) + I(x1^3) +
I(x2) + I(x2^2) + I(x2^3), data = df)
X_1r_orth <- model.matrix(Y ~ poly(x, 3) + poly(x1, 3) + poly(x2, 3), data = df)
## Mesmo modelo (3 restrições = spline cúbico "de fato"), duas bases: crua vs. bs()
X_3r_raw <- model.matrix(Y ~ poly(x, 3, raw = TRUE) + I(x1^3) + I(x2^3), data = df)
X_3r_bs <- model.matrix(Y ~ bs(x, knots = c(3.5, 7)), data = df)
data.frame(
modelo = c("1 restrição, base crua", "1 restrição, poly() ortogonal",
"3 restrições, base truncada crua", "3 restrições, bs()"),
numero_condicao = c(kappa(crossprod(X_1r_raw)), kappa(crossprod(X_1r_orth)),
kappa(crossprod(X_3r_raw)), kappa(crossprod(X_3r_bs)))
)
## modelo numero_condicao
## 1 1 restrição, base crua 1.586891e+08
## 2 1 restrição, poly() ortogonal 1.537788e+07
## 3 3 restrições, base truncada crua 4.835958e+07
## 4 3 restrições, bs() 4.626399e+02
A diferença é enorme: para o modelo de 3 restrições (o spline cúbico
final), o número de condição cai de ~48 milhões (base
crua) para ~463 (bs()) — mais de 5 ordens
de grandeza. E, apesar disso, os dois ajustes são
idênticos:
m_raw <- lm(Y ~ poly(x, 3, raw = TRUE) + I(x1^3) + I(x2^3), data = df)
m_bs <- lm(Y ~ bs(x, knots = c(3.5, 7)), data = df)
c(logLik_raw = as.numeric(logLik(m_raw)), logLik_bs = as.numeric(logLik(m_bs)))
## logLik_raw logLik_bs
## -40.06912 -40.06912
max(abs(fitted(m_raw) - fitted(m_bs)))
## [1] 2.9865e-14
Ou seja: a instabilidade numérica é um problema de como
calcular \(\hat\beta\), não de
o que o modelo representa. poly() (base
ortogonal) já ajuda bastante (comparado à base crua), mas é
bs() — a base B-spline do pacote splines — que
resolve o problema de forma robusta, e é por isso que ela é a escolha
padrão na prática a partir de poucos nós.
bs(): nós explícitos, nós automáticos e
Boundary.knotsfit_bs_k <- lm(Y ~ bs(x, knots = c(3.5, 7), Boundary.knots = c(0, 10)), data = df)
Em vez de escolher os nós manualmente, bs(x, df = ...)
os posiciona automaticamente nos quantis de \(x\):
fit_bs_df5 <- lm(Y ~ bs(x, df = 5, Boundary.knots = c(0, 10)), data = df)
attr(with(df, bs(x, df = 5, Boundary.knots = c(0, 10))), "knots")
## [1] 3.544345 6.239567
Com df=5 (e grau 3), bs() escolheu
automaticamente 2 nós internos, nos quantis de x —
próximos, mas não iguais, aos \(3{,}5\)
e \(7\) escolhidos “à mão”.
Boundary.knots merece atenção: se omitido,
bs() usa o intervalo amostral de \(x\) (range(df$x) \(\approx\) 0.28, 9.95), não o domínio
pretendido do problema (\([0,10]\), por construção da simulação).
Definir Boundary.knots explicitamente como o domínio
pretendido evita que a base mude de forma sutil dependendo de onde a
amostra por acaso começou e terminou — e, como veremos na Seção
“Extrapolação”, também importa para o que acontece fora da amostra.
ns(): splines cúbicos naturaisns() (natural splines) adiciona uma restrição
extra às já vistas: força a 2ª e a 3ª derivadas a serem zero
além dos nós de fronteira — ou seja, a curva é exatamente
linear fora do intervalo definido por
Boundary.knots. Isso custa 2 graus de liberdade a mais que
bs() para o mesmo conjunto de nós.
fit_ns_k <- lm(Y ~ ns(x, knots = c(3.5, 7), Boundary.knots = c(0, 10)), data = df)
fit_ns_df5 <- lm(Y ~ ns(x, df = 5, Boundary.knots = c(0, 10)), data = df)
attr(with(df, ns(x, df = 5, Boundary.knots = c(0, 10))), "knots")
## [1] 1.802739 3.857011 5.184644 8.104730
data.frame(
modelo = c("bs 2 nós", "bs df=5", "ns 2 nós", "ns df=5", "cúbica global"),
gl = sapply(list(fit_bs_k, fit_bs_df5, fit_ns_k, fit_ns_df5, fit3), function(m) length(coef(m))),
logLik = round(sapply(list(fit_bs_k, fit_bs_df5, fit_ns_k, fit_ns_df5, fit3), function(m) as.numeric(logLik(m))), 2),
AIC = round(sapply(list(fit_bs_k, fit_bs_df5, fit_ns_k, fit_ns_df5, fit3), AIC), 2)
)
## modelo gl logLik AIC
## 1 bs 2 nós 6 -40.07 94.14
## 2 bs df=5 6 -38.92 91.84
## 3 ns 2 nós 4 -65.70 141.39
## 4 ns df=5 6 -40.24 94.47
## 5 cúbica global 4 -66.57 143.13
Este resultado é instrutivo: ns() com os mesmos
2 nós de bs() tem log-verossimilhança bem pior
(\(-65{,}7\) contra \(-40{,}1\)) — a restrição de linearidade nas
bordas, com poucos nós, custa caro em ajuste. Mas ns(df=5)
posiciona automaticamente 4 nós internos (mais que os 2
de bs(df=5)) e recupera um ajuste quase idêntico ao de
bs() (\(-40{,}2\) contra
\(-38{,}9\)). Ou seja:
ns() geralmente precisa de mais nós que
bs() para o mesmo ajuste, porque parte da
flexibilidade vai para a restrição de linearidade nas bordas — a Seção
“Extrapolação” mostra por que, mesmo assim, costuma valer a pena.
bs() e ns()
calculamAs colunas de bs()/ns() são funções-base
fixas de \(x\) — o mesmo tipo de
matplot() de colunas usado em xySpline.R para
as bases de potências truncadas. Aqui estão as bases correspondentes aos
ajustes acima:
par(mfrow = c(1, 2))
xs <- seq(0, 10, length = 501)
matplot(xs, bs(xs, knots = c(3.5, 7), Boundary.knots = c(0, 10)), type = "l",
main = "bs(), 2 nós", xlab = "x", ylab = "")
abline(v = c(3.5, 7), lty = 3, col = "gray")
matplot(xs, ns(xs, knots = c(3.5, 7), Boundary.knots = c(0, 10)), type = "l",
main = "ns(), 2 nós", xlab = "x", ylab = "")
abline(v = c(3.5, 7), lty = 3, col = "gray")
Funções base de bs() (esquerda) e ns() (direita), mesmos nós (3.5, 7)
par(mfrow = c(1, 1))
Repare como, na base de ns() (direita), cada função-base
vira uma reta fora do intervalo \([0,10]\) — a restrição de linearidade
“embutida” na própria base, visível diretamente, sem precisar olhar
coeficiente algum.
predict(..., interval = "confidence") dá a incerteza
sobre a função média \(E[Y\mid x]\);
interval = "prediction" soma a variância residual e dá a
incerteza sobre uma nova observação individual — por
isso a banda de predição é sempre mais larga.
nd_ic <- data.frame(x = seq(0, 10, length = 201))
nd_ic <- transform(nd_ic, x1 = ifelse(x < 3.5, 0, x - 3.5), x2 = ifelse(x < 7, 0, x - 7))
ic_r3 <- predict(m3_r3, newdata = nd_ic, interval = "confidence")
ic_p4 <- predict(fit4, newdata = nd_ic, interval = "confidence")
ip_r3 <- predict(m3_r3, newdata = nd_ic, interval = "prediction")
ip_p4 <- predict(fit4, newdata = nd_ic, interval = "prediction")
par(mfrow = c(1, 2))
with(df, plot(Y ~ x, pch = 20, col = "gray40", main = "Confiança"))
matlines(nd_ic$x, ic_r3, col = "darkgreen", lty = c(1, 2, 2))
matlines(nd_ic$x, ic_p4, col = "steelblue", lty = c(1, 2, 2))
legend("topleft", c("spline cúbica (r3)", "quártica global"), col = c("darkgreen", "steelblue"), lty = 1, bty = "n", cex = 0.8)
with(df, plot(Y ~ x, pch = 20, col = "gray40", main = "Predição"))
matlines(nd_ic$x, ip_r3, col = "darkgreen", lty = c(1, 2, 2))
matlines(nd_ic$x, ip_p4, col = "steelblue", lty = c(1, 2, 2))
Intervalos de confiança (esquerda) e de predição (direita): spline cúbica (r3) vs. quártica global
par(mfrow = c(1, 1))
Dentro do domínio observado, as bandas de confiança da spline cúbica e da quártica global são parecidas (ambas capturam bem a curvatura); a diferença relevante entre elas aparece só quando se sai do domínio observado — assunto da próxima seção.
Estendendo a predição de \([0,10]\)
para \([-1,11]\) — só 1 unidade além de
cada lado — compara-se bs() (2 nós), bs() (1
nó), ns(df=5) e o polinômio global de grau 4:
fit_bs1 <- lm(Y ~ bs(x, knots = 5, Boundary.knots = c(0, 10)), data = df)
nd_ex <- data.frame(x = seq(-1, 11, length = 401))
nd_ex <- transform(nd_ex, Ytrue = 0.2 * x + cos(x + 1))
ex_bs2 <- predict(fit_bs_k, newdata = nd_ex, interval = "confidence")
## Warning in bs(x, degree = 3L, knots = c(3.5, 7), Boundary.knots = c(0, 10:
## alguns valores de 'x' depois dos nós de fronteira podem causar bases má
## condicionadas
ex_bs1 <- predict(fit_bs1, newdata = nd_ex, interval = "confidence")
## Warning in bs(x, degree = 3L, knots = 5, Boundary.knots = c(0, 10), intercept =
## FALSE): alguns valores de 'x' depois dos nós de fronteira podem causar bases má
## condicionadas
ex_ns5 <- predict(fit_ns_df5, newdata = nd_ex, interval = "confidence")
ex_p4 <- predict(fit4, newdata = nd_ex, interval = "confidence")
with(df, plot(Y ~ x, pch = 20, col = "gray40", xlim = c(-1, 11), ylim = c(-5, 15),
main = "Extrapolação: bs() x ns() x polinômio"))
with(nd_ex, lines(Ytrue ~ x, lty = 3))
matlines(nd_ex$x, ex_bs2, col = "tomato", lty = c(1, 2, 2))
matlines(nd_ex$x, ex_bs1, col = "purple", lty = c(1, 2, 2))
matlines(nd_ex$x, ex_ns5, col = "darkgreen", lty = c(1, 2, 2))
matlines(nd_ex$x, ex_p4, col = "steelblue", lty = c(1, 2, 2))
abline(v = c(0, 10), lty = 3, col = "gray60")
legend("top", c("bs 2 nós", "bs 1 nó", "ns df=5", "poly grau 4"),
col = c("tomato", "purple", "darkgreen", "steelblue"), lty = 1, bty = "n", ncol = 4, cex = 0.75)
Extrapolação além do domínio [0,10]: bs() e polinômio disparam; ns() se mantém estável
bs() emite, no processo, um aviso: “alguns valores
de ‘x’ depois dos nós de fronteira podem causar bases mal
condicionadas” — um alerta explícito do próprio pacote de que ele
não foi desenhado para extrapolar. ns()
não emite esse aviso, exatamente porque foi desenhado
para isso. Comparando as previsões (e bandas) 1 unidade além de cada
extremo do domínio:
nd_pts <- data.frame(x = c(-1, 0, 10, 11))
tab_ex <- rbind(
data.frame(modelo = "bs 2 nós", round(predict(fit_bs_k, newdata = nd_pts, interval = "confidence"), 2)),
data.frame(modelo = "bs 1 nó", round(predict(fit_bs1, newdata = nd_pts, interval = "confidence"), 2)),
data.frame(modelo = "ns df=5", round(predict(fit_ns_df5, newdata = nd_pts, interval = "confidence"), 2)),
data.frame(modelo = "poly grau4", round(predict(fit4, newdata = nd_pts, interval = "confidence"), 2))
)
## Warning in bs(x, degree = 3L, knots = c(3.5, 7), Boundary.knots = c(0, 10:
## alguns valores de 'x' depois dos nós de fronteira podem causar bases má
## condicionadas
## Warning in bs(x, degree = 3L, knots = 5, Boundary.knots = c(0, 10), intercept =
## FALSE): alguns valores de 'x' depois dos nós de fronteira podem causar bases má
## condicionadas
tab_ex$x <- rep(nd_pts$x, 4)
tab_ex[, c("modelo", "x", "fit", "lwr", "upr")]
## modelo x fit lwr upr
## 1 bs 2 nós -1 8.65 4.58 12.72
## 2 bs 2 nós 0 2.16 0.80 3.52
## 3 bs 2 nós 10 3.06 2.28 3.84
## 4 bs 2 nós 11 9.33 6.37 12.30
## 11 bs 1 nó -1 7.51 4.91 10.10
## 21 bs 1 nó 0 1.96 0.91 3.02
## 31 bs 1 nó 10 2.78 2.10 3.45
## 41 bs 1 nó 11 6.98 5.22 8.74
## 12 ns df=5 -1 2.50 0.49 4.50
## 22 ns df=5 0 0.95 -0.11 2.02
## 32 ns df=5 10 2.51 1.85 3.18
## 42 ns df=5 11 4.10 2.88 5.32
## 13 poly grau4 -1 9.95 6.89 13.01
## 23 poly grau4 0 2.37 1.24 3.49
## 33 poly grau4 10 2.96 2.26 3.66
## 43 poly grau4 11 8.99 6.89 11.09
Em \(x=-1\) (só 1 unidade fora da
amostra), a spline bs() de 2 nós prevê \(\hat Y\approx 8{,}6\) e o polinômio de grau
4 prevê \(\hat Y\approx 10{,}0\) —
ambos claramente absurdos, considerando que os valores observados nas
proximidades de \(x=0\) giram em torno
de \(1\)–\(2\). O ns(df=5) prevê \(\hat Y\approx 2{,}5\): ainda uma
extrapolação (não é mágica — é só uma reta a partir do que foi observado
perto da borda), mas muito mais razoável, com banda de confiança bem
mais estreita. O mesmo padrão se repete em \(x=11\). Esta é a razão prática mais forte
para preferir ns() a bs() (ou a um polinômio
global) sempre que houver alguma chance de predição fora — ou perto das
bordas — do domínio observado.
| Tópico | Ferramenta / resultado |
|---|---|
| Instabilidade numérica | base truncada crua tem \(\kappa(X'X)\) ~5 ordens de grandeza
maior que bs(), para o mesmo modelo |
Nós de bs()/ns() |
knots= (manual) ou df= (automático,
quantis de \(x\));
Boundary.knots deve refletir o domínio pretendido, não só a
amostra |
ns() vs. bs() |
ns() força linearidade além das bordas — custa ajuste
com poucos nós, mas estabiliza a extrapolação |
| Funções base | visíveis diretamente via
matplot(bs(...))/matplot(ns(...)) — a
restrição de ns() aparece literalmente como retas fora do
domínio |
| IC vs. IP | confiança = incerteza da média; predição = confiança + variância residual (sempre mais larga) |
| Extrapolação | bs() e polinômios globais podem divergir fortemente
fora do domínio; ns() é a opção estável por construção |
Com isso, fecha-se o conjunto de tópicos sinalizados como pendentes
ao longo da série (regressao-partes-texto.qmd,
regressao-partes23-eolico.qmd) — os únicos tópicos ainda
não cobertos são a extensão para múltiplos nós livres em graus
maiores e a conexão com P-splines/penalização,
já introduzida conceitualmente (mas não implementada) em
regressao-partes-texto.qmd.