@@ -22,6 +22,7 @@ library(ggdag)
2222library(ggrepel)
2323
2424library(rethinking)
25+ detach(package:rethinking, unload = T)
2526data(milk)
2627
2728at <- 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+
152164Simplest 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}
169195b7.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
192374ggplot() +
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