Skip to content

Commit 9b175d5

Browse files
polynomial comparison ch7
1 parent 7f71d99 commit 9b175d5

3 files changed

Lines changed: 286 additions & 49 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@ _site/
2424
*.html
2525

2626
# ===============================
27-
# Model fits / large objects
27+
# Model fits / images / large objects
2828
# ===============================
2929
fits/
30+
plots/
3031
*.rds
3132
*.RData
3233
*.rda
3334

35+
36+
37+
3438
# ===============================
3539
# Miscellaneous
3640
# ===============================

ch7ModelComparison.qmd

Lines changed: 275 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ library(ggdag)
2222
library(ggrepel)
2323
2424
library(rethinking)
25+
detach(package:rethinking, unload = T)
2526
data(milk)
2627
2728
at <- c(-3, -2, -1, 0, 1, 2, 3)
@@ -142,13 +143,24 @@ speciesBrain <-
142143
brain = c(438, 452, 612, 521, 752, 871, 1350),
143144
mass = c(37.0, 35.5, 34.5, 41.5, 55.5, 61.0, 53.5))
144145
145-
ggplot(data = speciesBrain, aes(x = mass, y = brain))+
146-
geom_point(shape = 21, color = "blue4", fill = "white")+
146+
p1 <- ggplot(data = speciesBrain, aes(x = mass, y = brain))+
147+
geom_point(shape = 21, color = "dodgerblue2", fill = "blue4", size =3)+
147148
theme_minimal()+
148149
ggrepel::geom_text_repel(aes(label = species), size = 3)+
149150
labs(x = "body mass (kg)", y = "brain volume (cc)")
151+
152+
p1
153+
154+
p1_built <- ggplot_build(p1)
155+
# Extract the axis limits and breaks
156+
x_limits <- p1_built$layout$panel_params[[1]]$x.range
157+
y_limits <- p1_built$layout$panel_params[[1]]$y.range
158+
159+
x_limits_z <- (x_limits - mean(speciesBrain$mass))/sd(speciesBrain$mass)
150160
```
151161

162+
### Linear
163+
152164
Simplest model is a linear one.
153165

154166
$$\text{Brain Size}_i \sim \text{Normal}(\mu_i, \sigma)$$
@@ -166,72 +178,289 @@ speciesBrain <- speciesBrain %>%
166178
mutate(mass_z = (mass - mean(mass))/sd(mass),
167179
brain_z = (brain - mean(brain))/sd(brain))
168180
181+
R2_fun<- function(brm_fit, seed = 7, ...) {
182+
set.seed(seed)
183+
p <- predict(brm_fit, summary = F, ...)
184+
# in my experience, it's more typical to define residuals as the criterion minus the predictions
185+
r <- speciesBrain$brain_z - apply(p, 2, mean)
186+
1 - rethinking::var2(r) / rethinking::var2(speciesBrain$brain_z )
187+
}
188+
189+
190+
```
191+
192+
193+
194+
```{r make poly models}
169195
b7.0 <- brm(data = speciesBrain,
170196
family = gaussian,
171197
brain_z ~ 1 + mass_z,
172198
prior = c(
173199
prior(normal(0.5, 1), class = Intercept),
174-
prior(normal(0, 1), class = b),
200+
prior(normal(0, 3), class = b),
201+
prior(lognormal(0,1), class = sigma)
202+
),
203+
iter = 2000, warmup = 500, seed = 4, cores = 4,
204+
backend = "cmdstanr", silent = 2, file = "fits/b07.0.6"
205+
)
206+
207+
#quadratic
208+
b7.1 <- brm(data = speciesBrain,
209+
family = gaussian,
210+
brain_z ~ 1 + mass_z + I(mass_z^2),
211+
prior = c(
212+
prior(normal(0.5, 1), class = Intercept),
213+
prior(normal(0, 3), class = b),
214+
prior(lognormal(0,1), class = sigma)
215+
),
216+
iter = 2000, warmup = 500, seed = 4, cores = 4,
217+
backend = "cmdstanr", silent = 2, file = "fits/b07.1.0"
218+
)
219+
220+
#cubic
221+
b7.2 <- brm(data = speciesBrain,
222+
family = gaussian,
223+
brain_z ~ 1 + mass_z + I(mass_z^2) + I(mass_z^3),
224+
prior = c(
225+
prior(normal(0.5, 1), class = Intercept),
226+
prior(normal(0, 3), class = b),
227+
prior(lognormal(0,1), class = sigma)
228+
),
229+
iter = 2000, warmup = 500, seed = 4, cores = 4,
230+
backend = "cmdstanr", silent = 2, file = "fits/b07.2.0"
231+
)
232+
233+
b7.3 <- brm(data = speciesBrain,
234+
family = gaussian,
235+
brain_z ~ 1 + mass_z + I(mass_z^2) + I(mass_z^3) + I(mass_z^4),
236+
prior = c(
237+
prior(normal(0.5, 1), class = Intercept),
238+
prior(normal(0, 3), class = b),
175239
prior(lognormal(0,1), class = sigma)
176240
),
177241
iter = 2000, warmup = 500, seed = 4, cores = 4,
178-
backend = "cmdstanr", silent = 2, file = "fits/b07.0.5"
242+
backend = "cmdstanr", silent = 2, file = "fits/b07.3.0"
243+
)
244+
245+
b7.4 <- brm(data = speciesBrain,
246+
family = gaussian,
247+
brain_z ~ 1 + mass_z + I(mass_z^2) + I(mass_z^3) + I(mass_z^4) + I(mass_z^5),
248+
prior = c(
249+
prior(normal(0.5, 1), class = Intercept),
250+
prior(normal(0, 3), class = b),
251+
prior(lognormal(0,1), class = sigma)
252+
),
253+
iter = 2000, warmup = 500, seed = 4, cores = 4, adapt_delta = 0.99,
254+
backend = "cmdstanr", silent = 2, file = "fits/b07.4.1",
255+
)
256+
257+
b7.5 <- brm(data = speciesBrain,
258+
family = gaussian,
259+
brain_z ~ 1 + mass_z + I(mass_z^2) + I(mass_z^3) + I(mass_z^4) + I(mass_z^5) + I(mass_z^6),
260+
prior = c(
261+
prior(normal(0.5, 1), class = Intercept),
262+
prior(normal(0, 3), class = b),
263+
prior(lognormal(0,1), class = sigma)
264+
),
265+
iter = 2000, warmup = 500, seed = 4, cores = 4, adapt_delta = 0.999, max_treedepth = 15,
266+
backend = "cmdstanr", silent = 2, file = "fits/b07.5.2"
179267
)
180268
181-
b7.0_sim <- as_tibble(b7.0) %>%
182-
mutate(simMass = seq(from = -3, to = 3, length.out = n()),
183-
simBrainEst = Intercept + (b_mass_z * simMass),
184-
simBrain = rnorm(n(), simBrainEst, sd = sigma))
185269
186270
```
187271

188-
```{r}
189-
#| fig-width: 6
190-
#| fig-height: 4
272+
```{r get heat map draws}
273+
274+
simulate_brain_model <- function(model_samples, polynomial_terms = 1) {
275+
as_tibble(model_samples) %>%
276+
mutate(
277+
simMass = seq(from = min(speciesBrain$mass_z), to = max(speciesBrain$mass_z), length.out = n()),
278+
simBrainEst = if (polynomial_terms == 1) {
279+
Intercept + (b_mass_z * simMass)
280+
} else if (polynomial_terms == 2) {
281+
Intercept + (b_mass_z * simMass) + (b_Imass_zE2 * simMass^2)
282+
} else if (polynomial_terms == 3) {
283+
Intercept + (b_mass_z * simMass) + (b_Imass_zE2 * simMass^2) + (b_Imass_zE3 * simMass^3)
284+
} else if (polynomial_terms == 4) {
285+
Intercept + (b_mass_z * simMass) + (b_Imass_zE2 * simMass^2) + (b_Imass_zE3 * simMass^3) + (b_Imass_zE4 * simMass^4)
286+
} else if (polynomial_terms == 5) {
287+
Intercept + (b_mass_z * simMass) + (b_Imass_zE2 * simMass^2) + (b_Imass_zE3 * simMass^3) + (b_Imass_zE4 * simMass^4) + (b_Imass_zE5 * simMass^5)
288+
} else if (polynomial_terms == 6) {
289+
Intercept + (b_mass_z * simMass) + (b_Imass_zE2 * simMass^2) + (b_Imass_zE3 * simMass^3) + (b_Imass_zE4 * simMass^4) + (b_Imass_zE5 * simMass^5) + (b_Imass_zE6 * simMass^6)
290+
},
291+
simBrain = rnorm(n(), simBrainEst, sd = sigma),
292+
simMass_orig = simMass * sd(speciesBrain$mass) + mean(speciesBrain$mass),
293+
simBrainEst_orig = simBrainEst * sd(speciesBrain$brain) + mean(speciesBrain$brain),
294+
simBrain_orig = simBrain * sd(speciesBrain$brain) + mean(speciesBrain$brain)
295+
)
296+
}
297+
298+
b7.0_sim <- simulate_brain_model(b7.0, polynomial_terms = 1)
299+
b7.1_sim <- simulate_brain_model(b7.1, polynomial_terms = 2)
300+
b7.2_sim <- simulate_brain_model(b7.2, polynomial_terms = 3)
301+
b7.3_sim <- simulate_brain_model(b7.3, polynomial_terms = 4)
302+
b7.4_sim <- simulate_brain_model(b7.4, polynomial_terms = 5)
303+
b7.5_sim <- simulate_brain_model(b7.5, polynomial_terms = 6)
304+
305+
```
306+
307+
```{r draw lines}
308+
#| eval: false
309+
310+
new_data_sim <- tibble(mass_z = seq(from = x_limits_z[[1]], to = x_limits_z[[2]], length.out = 60))
311+
brain_loo_lines <- function(brms_fit, row, ...) {
312+
313+
# # refit the model
314+
new_fit <-
315+
update(brms_fit,
316+
newdata = filter(speciesBrain, row_number() != row),
317+
iter = 2000, warmup = 1000, chains = 4, cores = 4,
318+
seed = 7,
319+
refresh = 0,
320+
...)
321+
322+
# pull the lines values
323+
fitted(new_fit,
324+
newdata = new_data_sim) %>%
325+
data.frame() %>%
326+
select(Estimate) %>%
327+
bind_cols(new_data_sim)
328+
}
329+
330+
331+
b7.0_fits <-
332+
tibble(row = 1:7) %>%
333+
mutate(post = purrr::map(row, ~brain_loo_lines(brms_fit = b7.0, row = .))) %>%
334+
unnest(post) %>%
335+
filter(Estimate >= min(speciesBrain$brain_z) - (1.5 *sd(speciesBrain$brain_z)) & Estimate <= max(speciesBrain$brain_z) + (1.5 *sd(speciesBrain$brain_z)))
336+
337+
b7.1_fits <-
338+
tibble(row = 1:7) %>%
339+
mutate(post = purrr::map(row, ~brain_loo_lines(brms_fit = b7.1, row = .))) %>%
340+
unnest(post) %>%
341+
filter(Estimate >= min(speciesBrain$brain_z) -(1.5 * sd(speciesBrain$brain_z)) & Estimate <= max(speciesBrain$brain_z) + (1.5 *sd(speciesBrain$brain_z)))
342+
343+
b7.2_fits <-
344+
tibble(row = 1:7) %>%
345+
mutate(post = purrr::map(row, ~brain_loo_lines(brms_fit = b7.2, row = .))) %>%
346+
unnest(post) %>%
347+
filter(Estimate >= min(speciesBrain$brain_z) - (1.5 *sd(speciesBrain$brain_z)) & Estimate <= max(speciesBrain$brain_z) + (1.5 *sd(speciesBrain$brain_z)))
348+
349+
b7.3_fits <-
350+
tibble(row = 1:7) %>%
351+
mutate(post = purrr::map(row, ~brain_loo_lines(brms_fit = b7.3, row = .))) %>%
352+
unnest(post) %>%
353+
filter(Estimate >= min(speciesBrain$brain_z) - (1.5 *sd(speciesBrain$brain_z)) & Estimate <= max(speciesBrain$brain_z) + (1.5 *sd(speciesBrain$brain_z)))
354+
355+
b7.4_fits <-
356+
tibble(row = 1:7) %>%
357+
mutate(post = purrr::map(row, ~brain_loo_lines(brms_fit = b7.4, row = .))) %>%
358+
unnest(post) %>%
359+
filter(Estimate >= min(speciesBrain$brain_z) - (1.5 *sd(speciesBrain$brain_z)) & Estimate <= max(speciesBrain$brain_z) + (1.5 *sd(speciesBrain$brain_z)))
360+
361+
362+
b7.5_fits <-
363+
tibble(row = 1:7) %>%
364+
mutate(post = purrr::map(row, ~brain_loo_lines(brms_fit = b7.5, row = .))) %>%
365+
unnest(post) %>%
366+
filter(Estimate >= min(speciesBrain$brain_z) - (1.5 * sd(speciesBrain$brain_z)) & Estimate <= (1.5 * max(speciesBrain$brain_z) + sd(speciesBrain$brain_z)))
367+
368+
369+
```
370+
371+
372+
```{r linear plot}
191373
192374
ggplot() +
375+
labs(title = "Linear",x = "body mass (kg)", y = "brain volume (cc)") +
376+
theme_minimal() +
377+
guides(fill = "none")+
378+
coord_cartesian(xlim = x_limits * c(0.99, 1.01),
379+
ylim = y_limits * c(0.99, 1.01)) +
193380
stat_density_2d(data = b7.0_sim,
194-
aes(x = simMass, y = simBrainEst, fill = after_stat(ndensity)),
195-
geom = "raster", contour = FALSE) +
381+
aes(x = simMass_orig, y = simBrainEst_orig, fill = after_stat(ndensity)),
382+
geom = "raster", contour = FALSE) +
196383
scale_fill_viridis_c(option = "magma") +
197384
geom_point(data = speciesBrain,
198-
aes(x = mass_z, y = brain_z),
199-
shape = 21, color = "white", fill = "black", lwd = 3, alpha = 1, size =2.5)+
200-
labs(title = "brain ~ mass", subtitle = "Mu estimate") +
201-
theme_minimal()+
202-
ggrepel::geom_text_repel(data = speciesBrain, aes(x = mass_z, y = brain_z, label = species), color = "white", size = 3)+
203-
# xlim(c(-3,3))+
204-
# ylim(c(-3,3))+
205-
scale_x_continuous("body mass (kg)",
206-
breaks = at,
207-
labels = round(at * sd(speciesBrain$mass) +
208-
mean(speciesBrain$mass)),
209-
limits = c(min(speciesBrain$mass_z), max(speciesBrain$mass_z))* 1.5 * sd(speciesBrain$brain_z)) +
210-
scale_y_continuous("brain volume (cc)",
211-
breaks = at,
212-
labels = round(at * sd(speciesBrain$brain) +
213-
mean(speciesBrain$brain)),
214-
limits = c(min(speciesBrain$brain_z), max(speciesBrain$brain_z)) * 1.5 * sd(speciesBrain$brain_z))+
215-
guides(fill = "none")
385+
aes(x = mass, y = brain),
386+
shape = 21, color = "white", fill = "black", lwd = 3, alpha = 1, size = 2.5) +
387+
ggrepel::geom_text_repel(data = speciesBrain,
388+
aes(x = mass, y = brain, label = species),
389+
size = 4, color = "white",
390+
box.padding = 2.5, point.padding = 1)
391+
```
392+
This model has an $R^2$ value of `r round(R2_fun(b7.0), 2)`
216393

394+
### Polynomial
217395

396+
For some comparison models we can add polynomial terms of *Mass* to our regression. For example the scond degee polynomial that relates body size to brain size is a parabola which has this form:
397+
398+
$$\mu_i = \alpha + \beta_1\text{Body Mass}_i + \beta_2(\text{Body Mass}_i)^2$$
399+
We'll make 4 more models like this each adding another polynomial term like $\beta_3(\text{Body Mass}_i)^3$, $\beta_4(\text{Body Mass}_i)^4$... etc
400+
401+
Below are our six models posterior $mu$'s plotted with the data. *Note this is not the posterior predictive distribution since it doesn't include $sigma$
402+
403+
In the figure below I've plotted both the heat map for where the model thinks the $\mu$ estimate should be given all of the data. Additionally I've plotted golden lines where I get the best fit lines if I refitted the model by leaving out a different data point each time. This is a "Leave One Out" (LOO) sort of method that helps us see how good our model would do if we didn't have one of our data points and we wanted to predict it. There are seven of these best fit lines in each model because there are seven different data points we can drop.
404+
405+
For the simple linear 1st order regression we could remove any one point from the sample and get pretty much the same regression line. In contrast, the most complex model 6th order is very sensitive to the sample. The predicted mean would change course a lot, if we removed any one point from the sample. You can see the truth of this in the below plots. On the top left, each line is the best fit for the linear regression. The curves on the bottom right are instead different sixth-order polynomials. Notice that the straight lines hardly vary, while the curves fly about wildly.
406+
407+
408+
```{r}
409+
#| eval: false
410+
411+
base_plt2 <- ggplot() +
412+
# labs(title = "brain ~ mass", subtitle = "Mu estimate",
413+
# x = "body mass (kg)", y = "brain volume (cc)") +
414+
theme_minimal() +
415+
guides(fill = "none")+
416+
coord_cartesian(xlim = x_limits * c(0.995, 1.005),
417+
ylim = y_limits * c(0.995, 1.005))
418+
419+
add_heatmap_and_points <- function(base_plot, sim_data, fit_lines) {
420+
base_plot +
421+
stat_density_2d(data = sim_data,
422+
aes(x = simMass_orig, y = simBrainEst_orig, fill = after_stat(ndensity)),
423+
geom = "raster", contour = FALSE) +
424+
scale_fill_viridis_c(option = "magma") +
425+
geom_point(data = speciesBrain,
426+
aes(x = mass, y = brain),
427+
shape = 21, color = "white", fill = "black", lwd = 3, alpha = 1, size = 2.5) +
428+
theme(
429+
axis.text.x = element_blank(),
430+
axis.text.y = element_blank(),
431+
axis.ticks.x = element_blank(),
432+
axis.ticks.y = element_blank(),
433+
axis.title.x = element_blank(),
434+
axis.title.y = element_blank()
435+
) +
436+
geom_line(data = fit_lines, aes(x = (mass_z * sd(speciesBrain$mass)) + mean(speciesBrain$mass),
437+
y = (Estimate * sd(speciesBrain$brain)) + mean(speciesBrain$brain), group = row),
438+
color = "gold1", alpha = .5)
439+
}
440+
441+
p2.0 <- add_heatmap_and_points(base_plt2, b7.0_sim, b7.0_fits) + labs(title = "Linear") + theme(plot.title = element_text(size = 10))
442+
p2.1 <- add_heatmap_and_points(base_plt2, b7.1_sim, b7.1_fits) + labs(title = "Quadratic") + theme(plot.title = element_text(size = 10))
443+
p2.2 <- add_heatmap_and_points(base_plt2, b7.2_sim, b7.2_fits) + labs(title = "Cubic") + theme(plot.title = element_text(size = 10))
444+
p2.3 <- add_heatmap_and_points(base_plt2, b7.3_sim, b7.3_fits) + labs(title = "4th order") + theme(plot.title = element_text(size = 10))
445+
p2.4 <- add_heatmap_and_points(base_plt2, b7.4_sim, b7.4_fits) + labs(title = "5th order") + theme(plot.title = element_text(size = 10))
446+
p2.5 <- add_heatmap_and_points(base_plt2, b7.5_sim, b7.5_fits) + labs(title = "6th order") + theme(plot.title = element_text(size = 10))
447+
448+
449+
polyCompPlt <- p2.0 + p2.1 + p2.2 + p2.3 + p2.4 + p2.5
450+
451+
saveRDS(polyCompPlt, "plots/polyCompPlt.rds")
218452
```
219453

220454
```{r}
221-
mass_brain <- speciesBrain %>%
222-
rename("mass_obs" = mass_z,
223-
"brain_obs" = brain_z) %>%
224-
dplyr::select(c(mass_obs, brain_obs))
225-
226-
227-
# as_tibble(b7.0) %>%
228-
# # Add row numbers to track individual draws
229-
# mutate(draw_id = row_number()) %>%
230-
# # Create all combinations of draws and new X values
231-
# crossing(mass_brain) %>%
232-
# mutate(brain_mu_pred = b_Intercept + (b_mass_z * brain_obs),
233-
# brain_mu_resid= brain_obs - brain_pred_mu,
234-
# brain_rmse =
235-
# )
455+
#| fig-width: 10
456+
#| fig-height: 6
457+
458+
readRDS("plots/polyCompPlt.rds")
236459
```
237460

461+
462+
The overfit polynomial models manage to fit the data extremely well, but they suffer for this within-sample accuracy by making nonsensical out-of- sample predictions. In contrast, underfitting produces models that are inaccurate both within and out of sample. They have learned too little, failing to recover regular features of the sample.
463+
464+
Another perspective on the absurd models just above is to consider that model fitting can be considered a form of data compression. Parameters summarize relationships among the data. These summaries compress the data into a simpler form, although with loss of information (“lossy” compression) about the sample. The parameters can then be used to generate new data, effectively decompressing the data.
465+
466+
When a model has a parameter to correspond to each datum, then there is actually no compression. The model just encodes the raw data in a different form, using parameters instead. As a result, we learn nothing about the data from such a model. Learning about the data requires using a simpler model that achieves some compression, but not too much. This view of model selection is often known as Minimum Description Length (MDL).

0 commit comments

Comments
 (0)