├── .gitignore ├── AIR_presentation ├── AIR_presentation.Rmd ├── AIR_presentation.html ├── blinking_meme.jpg └── xaringan-themer-air.css ├── Intro.Rmd ├── Intro.html ├── Presentation_files └── xaringan-themer.css ├── README.md ├── RMAIR_presentation ├── RMAIR_presentation.Rmd ├── RMAIR_presentation.html ├── blinking_meme.jpg └── xaringan-themer-rmair.css ├── RUG_presentation ├── RUG_presentation.Rmd ├── RUG_presentation.html ├── blinking_meme.jpg └── xaringan-themer.css ├── R_code ├── fit_model.R └── simulate_data.R ├── data └── course_outcomes.csv ├── nomogram_explanation.Rmd ├── visual_folder ├── banana_graphs.R ├── colors.R ├── counterfactuals.R ├── log_odds.R ├── nomograms.R ├── odds.R ├── probability_baseline.R └── probability_group.R └── xaringan-themer.css /.gitignore: -------------------------------------------------------------------------------- 1 | *.Rproj 2 | .Rproj.user 3 | .Rhistory 4 | .RData 5 | .Ruserdata 6 | .DS_Store 7 | *.png 8 | */libs/* 9 | -------------------------------------------------------------------------------- /AIR_presentation/AIR_presentation.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Visualizing logistic regression results for non-technical audiences" 3 | author: "Abby Kaplan and Keiko Cawley
Salt Lake Community College" 4 | institute: "AIR Forum" 5 | date: "May 31, 2023" 6 | output: 7 | xaringan::moon_reader: 8 | lib_dir: libs 9 | css: ["xaringan-themer-air.css"] 10 | nature: 11 | highlightStyle: github 12 | highlightLines: true 13 | countIncrementalSlides: false 14 | 15 | --- 16 | 17 | class: inverse, center, middle 18 | 19 | # GitHub 20 | ### https://github.com/keikcaw/visualizing-logistic-regression 21 | 22 | `r knitr::opts_knit$set(root.dir='..')` 23 | 24 | ```{r setup, include=FALSE} 25 | options(htmltools.dir.version = FALSE) 26 | library(dplyr) 27 | library(ggplot2) 28 | library(tidyverse) 29 | library(lme4) 30 | library(logitnorm) 31 | library(kableExtra) 32 | library(cowplot) 33 | library(grid) 34 | library(gridExtra) 35 | library(patchwork) 36 | library(knitr) 37 | theme_set(theme_bw()) 38 | opts_chunk$set(echo = F, message = F, warning = F, error = F, fig.retina = 3, 39 | fig.align = "center", fig.width = 6, fig.asp = 0.618, 40 | out.width = "70%") 41 | ``` 42 | 43 | ```{css} 44 | .remark-slide-content h1 { 45 | margin-bottom: 0em; 46 | } 47 | .remark-code { 48 | font-size: 60% !important; 49 | } 50 | ``` 51 | 52 | --- 53 | 54 | class: inverse, center, middle 55 | 56 | # Logistic regression review 57 | 58 | --- 59 | # Logistic regression: Binary outcomes 60 | 61 | - Use logistic regression to model a binary outcome 62 | 63 | -- 64 | 65 | - Examples from higher education: 66 | 67 | -- 68 | 69 | - Did the student pass the class? 70 | 71 | -- 72 | 73 | - Did the student enroll for another term? 74 | 75 | -- 76 | 77 | - Did the student graduate? 78 | 79 | 80 | --- 81 | # The design of logistic regression 82 | 83 | - We want to model the probability that the outcome happened 84 | 85 | -- 86 | 87 | - But probabilities are bounded between 0 and 1 88 | 89 | -- 90 | 91 | - Instead, we model the logit of the probability: 92 | 93 | $$ 94 | \mbox{logit}(p) = \log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 95 | $$ 96 | 97 | --- 98 | 99 | class: inverse, center, middle 100 | 101 | # What's the problem? 102 | 103 | --- 104 | 105 | layout: true 106 | 107 | # Just tell me "the" effect 108 | 109 | - Stakeholders often want to know whether something affects outcomes, and by how much 110 | 111 | --- 112 | 113 | -- 114 | 115 | - But we don't model probabilities directly 116 | 117 | $$ 118 | \log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 119 | $$ 120 | 121 | --- 122 | 123 | - But we don't model probabilities directly 124 | 125 | $$ 126 | \boxed{\log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right)} = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 127 | $$ 128 | 129 | -- 130 | 131 | - We can solve for _p_: 132 | 133 | $$ 134 | \begin{aligned} 135 | p & = \mbox{logit}^{-1}(\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n) \\ & \\ 136 | & = \frac{e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}}{1 + e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}} 137 | \end{aligned} 138 | $$ 139 | 140 | --- 141 | 142 | layout: true 143 | 144 | # "The" effect is nonlinear in _p_ 145 | 146 | $$ 147 | \begin{aligned} 148 | p & = \frac{e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}}{1 + e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}} 149 | \end{aligned} 150 | $$ 151 | 152 | --- 153 | 154 | -- 155 | 156 | ```{r} 157 | logistic.curve.0.p = ggplot() + 158 | stat_function(fun = function(x) (exp(x)/(1+(exp(x)))), color = "#009DD8") + 159 | scale_x_continuous(expression(beta[0]+beta[1]~x[1]~'+'~'...+'~beta[n]~x[n]), 160 | limits = c(-6, 6), breaks = seq(-6, 6, 2)) + 161 | scale_y_continuous("probability (p)", limits = c(0, 1)) 162 | logistic.curve.0.p 163 | ``` 164 | 165 | --- 166 | 167 | ```{r} 168 | logistic.curve.1.p = logistic.curve.0.p + 169 | annotate("segment", x = -3, xend = -2, y = invlogit(-3), yend = invlogit(-3), 170 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 171 | annotate("segment", x = -2, xend = -2, y = invlogit(-3), yend = invlogit(-2), 172 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 173 | annotate("text", x = -3.5, y = 0.3, 174 | label = str_wrap("A 1-unit change here leads to a small change in probability", 175 | 20)) 176 | logistic.curve.1.p 177 | ``` 178 | 179 | --- 180 | 181 | ```{r} 182 | logistic.curve.2.p = logistic.curve.1.p + 183 | annotate("segment", x = 0, xend = 1, y = invlogit(0), yend = invlogit(0), 184 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 185 | annotate("segment", x = 1, xend = 1, y = invlogit(0), yend = invlogit(1), 186 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 187 | annotate("text", x = 3, y = 0.6, 188 | label = str_wrap("A 1-unit change here leads to a large change in probability", 189 | 20)) 190 | logistic.curve.2.p 191 | ``` 192 | 193 | --- 194 | 195 | layout: false 196 | 197 | class: inverse, center, middle 198 | 199 | # Sample dataset and model 200 | 201 | --- 202 | 203 | # Dataset 204 | 205 | ```{r fit_model, include = F} 206 | knitr::read_chunk("R_code/fit_model.R") 207 | ``` 208 | 209 | - Our simulated dataset describes students who took Balloon Animal-Making 201 at University Imaginary 210 | 211 | -- 212 | 213 | ```{r dataset_summary_table} 214 | table <- data.frame( 215 | Variable = c("Mac user", "Wear glasses", "Pet type", "Favorite color", "Prior undergraduate GPA", "Height", "Went to tutoring", "Passed"), 216 | Possible.Responses = c("TRUE/FALSE", "TRUE/FALSE", "dog, cat, fish, none", "blue, red, green, orange", "0.0-4.0", "54-77 inches", "TRUE/FALSE", "TRUE/FALSE"), 217 | Variable.Type = c("binary", "binary", "categorical", "categorical", "continuous", "continuous", "binary", "binary") 218 | ) 219 | table %>% 220 | kbl(col.names = gsub("[.]", " ", names(table))) %>% 221 | kable_styling(bootstrap_options = c("striped", "hover"), full_width = F) 222 | ``` 223 | 224 | --- 225 | 226 | # Model 227 | 228 | - Dependent variable: did the student pass? 229 | 230 | -- 231 | 232 | - Continuous variables were centered and standardized 233 | 234 | ```{r load-data} 235 | ``` 236 | 237 | ```{r prepare-data-continuous} 238 | ``` 239 | 240 | -- 241 | 242 | - Reference levels for categorical variables: 243 | 244 | -- 245 | 246 | - Pet type: none 247 | 248 | -- 249 | 250 | - Favorite color: blue 251 | 252 | ```{r prepare-data-categorical} 253 | ``` 254 | 255 | --- 256 | 257 | # Model 258 | 259 | ```{r model} 260 | ``` 261 | 262 | --- 263 | 264 | # Model 265 | 266 | ```{r model, highlight.output = c(4, 5, 7, 9, 11, 12, 13)} 267 | ``` 268 | 269 | 270 | --- 271 | 272 | # Causality disclaimer 273 | 274 | - Some visualizations strongly imply a causal interpretation 275 | 276 | -- 277 | 278 | - It's your responsibility to evaluate whether a causal interpretation is appropriate 279 | 280 | -- 281 | 282 | - If the data doesn't support a causal interpretation, **don't use a visualization that implies one** 283 | 284 | 285 | --- 286 | 287 | class: inverse, center, middle 288 | 289 | # Visualization family 1: 290 | 291 | # Presenting model coefficients 292 | 293 | --- 294 | 295 | # Coefficients in a table 296 | 297 | ``` {r get-coefficients, include = F} 298 | ``` 299 | 300 | ```{r} 301 | coefs.df %>% 302 | dplyr::select(-parameter) %>% 303 | kbl(col.names = c("Parameter", "Estimate", "Standard error", "z", "p")) %>% 304 | kable_styling(bootstrap_options = c("striped", "hover"), 305 | font_size = 14, full_width = F) %>% 306 | row_spec(0, align = "c") 307 | ``` 308 | 309 | --- 310 | 311 | # Coefficients in a table 312 | 313 | ![blinking_meme](blinking_meme.jpg) 314 | 315 | --- 316 | 317 | # Change in log odds 318 | 319 | ```{r colors, include = F} 320 | knitr::read_chunk("visual_folder/colors.R") 321 | ``` 322 | 323 | ```{r color-palette, include = F} 324 | ``` 325 | 326 | ```{r log_odds, include = F} 327 | knitr::read_chunk("visual_folder/log_odds.R") 328 | ``` 329 | 330 | ```{r change-in-log-odds, fig.show = "hide"} 331 | ``` 332 | 333 | ```{r change_in_log_odds_plot, out.width = "100%"} 334 | log.odds.p = log.odds.p + 335 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.1), clip = "off") 336 | log.odds.p.full = log.odds.p + 337 | ggtitle(str_wrap(gsub("\\n", " ", log.odds.p$labels$title), 50)) 338 | log.odds.p.full 339 | ``` 340 | 341 | --- 342 | 343 | # Change in log odds: Pros 344 | 345 | .pull-left[ 346 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 347 | log.odds.p.half = log.odds.p + 348 | guides(color = guide_legend(nrow = 3)) + 349 | theme(legend.position = "bottom") 350 | log.odds.p.half 351 | ``` 352 | ] 353 | 354 | -- 355 | 356 | .pull-right[ 357 | - It's clear which relationships are positive and which are negative 358 | 359 | {{content}} 360 | ] 361 | 362 | -- 363 | 364 | - The plot has a transparent relationship to the fitted model 365 | 366 | --- 367 | 368 | # Change in log odds: Pros 369 | 370 | .pull-left[ 371 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 372 | log.odds.p.half + 373 | annotate("rect", ymin = -1.2, ymax = 1.2, xmin = -0.5, xmax = 0.5, 374 | fill = NA, color = "red", size = 2) 375 | ``` 376 | ] 377 | 378 | .pull-right[ 379 | - It's clear which relationships are positive and which are negative 380 | 381 | - The plot has a transparent relationship to the fitted model 382 | 383 | - Numbers all in one place: a single scale instead of a table of numbers 384 | ] 385 | 386 | --- 387 | 388 | # Change in log odds: Cons 389 | 390 | .pull-left[ 391 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 392 | log.odds.p.half 393 | ``` 394 | ] 395 | 396 | --- 397 | 398 | # Change in log odds: Cons 399 | 400 | .pull-left[ 401 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 402 | log.odds.p.half + 403 | annotate("rect", ymin = -1, ymax = 0.57, xmin = -1.2, xmax = -0.2, 404 | fill = NA, color = "red", size = 2) 405 | ``` 406 | ] 407 | 408 | .pull-right[ 409 | - The magnitude of effect is in the log odds scale 410 | 411 | {{content}} 412 | ] 413 | 414 | -- 415 | 416 | - What is a 0.4 change in the log odds? 417 | 418 | {{content}} 419 | 420 | -- 421 | 422 | - Is the change between 0.4 and 0.8 log odds "big" or "small"? 423 | 424 | {{content}} 425 | 426 | -- 427 | 428 | - You probably don't want to give your audience a tutorial on the inverse logit function 429 | 430 | --- 431 | 432 | # Secret log odds 433 | 434 | ```{r change-in-log-odds-adjusted-axis, fig.show = "hide"} 435 | ``` 436 | 437 | ```{r change_in_log_odds_adjusted_axis_plot, out.width = "100%"} 438 | secret.log.odds.p = secret.log.odds.p + 439 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.5), clip = "off") 440 | secret.log.odds.p.full = secret.log.odds.p + 441 | ggtitle(str_wrap(gsub("\\n", " ", secret.log.odds.p$labels$title), 50)) 442 | secret.log.odds.p.full 443 | ``` 444 | 445 | --- 446 | 447 | # Secret log odds: Pros 448 | 449 | .pull-left[ 450 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 451 | secret.log.odds.p.half = secret.log.odds.p + 452 | guides(color = guide_legend(nrow = 3)) + 453 | theme(legend.position = "bottom") 454 | secret.log.odds.p.half 455 | ``` 456 | ] 457 | 458 | --- 459 | 460 | # Secret log odds: Pros 461 | 462 | .pull-left[ 463 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 464 | secret.log.odds.p.half + 465 | annotate("rect", ymin = -1.45, ymax = 1.45, xmin = -1.2, xmax = 0.5, 466 | fill = NA, color = "red", size = 2) 467 | ``` 468 | ] 469 | 470 | -- 471 | 472 | .pull-right[ 473 | - Easy: just relabel the x-axis 474 | 475 | {{content}} 476 | ] 477 | 478 | -- 479 | 480 | - No numbers for your audience to misinterpret 481 | 482 | --- 483 | 484 | # Secret log odds: Cons 485 | 486 | .pull-left[ 487 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 488 | secret.log.odds.p.half 489 | ``` 490 | ] 491 | 492 | -- 493 | 494 | .pull-right[ 495 | - Can't convey absolute magnitude of an effect 496 | 497 | {{content}} 498 | ] 499 | 500 | -- 501 | 502 | - Your audience might ask "where are the numbers?" anyway 503 | 504 | --- 505 | 506 | layout: true 507 | 508 | # Change in odds ratio 509 | 510 | - Your audience may be more familiar with the "odds" part of log odds 511 | 512 | --- 513 | 514 | --- 515 | 516 | $$\log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n$$ 517 | 518 | --- 519 | 520 | $$\log\left(\boxed{\begin{array}{c}\frac{p}{1 - p}\end{array}}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n$$ 521 | 522 | -- 523 | 524 | - Can't we just exponentiate to get the odds? 525 | 526 | -- 527 | 528 | $$\frac{p}{1 - p} = e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}$$ 529 | 530 | -- 531 | 532 | - Now the effect of a coefficient is multiplicative, not additive 533 | 534 | -- 535 | 536 | $$\frac{p}{1 - p} = e^{\beta_ix_i} \cdot e^{\beta_0 + \beta_1x_1 + \ldots + \beta_{i-1}x_{i-1} + \beta_{i+1}x_{i+1} + \ldots + \beta_nx_n}$$ 537 | 538 | --- 539 | 540 | layout: false 541 | 542 | # Change in odds ratio 543 | 544 | ```{r odds_ratio, include = F, cache = F} 545 | knitr::read_chunk("visual_folder/odds.R") 546 | ``` 547 | 548 | ```{r odds-ratio-adjusted-axis, fig.show = "hide"} 549 | ``` 550 | 551 | ```{r out.width = "100%"} 552 | odds.ratio.p = odds.ratio.p + 553 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 3), clip = "off") 554 | odds.ratio.p.full = odds.ratio.p + 555 | ggtitle(str_wrap(gsub("\\n", " ", odds.ratio.p$labels$title), 50)) 556 | odds.ratio.p.full 557 | ``` 558 | 559 | --- 560 | 561 | # Change in odds ratio: Pros 562 | 563 | .pull-left[ 564 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 565 | odds.ratio.p.half = odds.ratio.p + 566 | guides(color = guide_legend(nrow = 3)) + 567 | theme(legend.position = "bottom") 568 | odds.ratio.p.half 569 | ``` 570 | ] 571 | 572 | -- 573 | 574 | .pull-right[ 575 | - Changes in odds might be easier to describe than changes in log odds 576 | 577 | {{content}} 578 | ] 579 | 580 | -- 581 | 582 | - Still pretty easy: a simple transformation of your model coefficients 583 | 584 | --- 585 | 586 | # Change in odds ratio: Cons 587 | 588 | .pull-left[ 589 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 590 | odds.ratio.p.half 591 | ``` 592 | ] 593 | 594 | -- 595 | 596 | .pull-right[ 597 | - Not the way we usually describe odds 598 | ] 599 | 600 | --- 601 | 602 | # Change in odds ratio: Cons 603 | 604 | .pull-left[ 605 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 606 | odds.ratio.p.half 607 | ``` 608 | ] 609 | 610 | .pull-right[ 611 | - Not the way we usually describe odds 612 | 613 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 614 | ] 615 | 616 | --- 617 | 618 | # Change in odds ratio: Cons 619 | 620 | .pull-left[ 621 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 622 | odds.ratio.p.half 623 | ``` 624 | ] 625 | 626 | .pull-right[ 627 | - Not the way we usually describe odds 628 | 629 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 630 | 631 | - The unfamiliar format may undo the benefit of using a familiar concept 632 | 633 | {{content}} 634 | ] 635 | 636 | -- 637 | 638 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 639 | 640 | --- 641 | 642 | # Change in odds ratio: Cons 643 | 644 | .pull-left[ 645 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 646 | odds.ratio.p.half + 647 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = -1.2, xmax = 0.5, 648 | fill = NA, color = "red", size = 2) 649 | ``` 650 | ] 651 | 652 | .pull-right[ 653 | - Not the way we usually describe odds 654 | 655 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 656 | 657 | - The unfamiliar format may undo the benefit of using a familiar concept 658 | 659 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 660 | 661 | - Percent change in odds (300% = triple the odds) might be misinterpreted as a probability 662 | ] 663 | 664 | --- 665 | 666 | # Change in odds ratio: Cons 667 | 668 | .pull-left[ 669 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 670 | odds.ratio.p.half + 671 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = -1.2, xmax = 0.5, 672 | fill = NA, color = "red", size = 2) 673 | ``` 674 | ] 675 | 676 | .pull-right[ 677 | - Not the way we usually describe odds 678 | 679 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 680 | 681 | - The unfamiliar format may undo the benefit of using a familiar concept 682 | 683 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 684 | 685 | - Percent change in odds (300% = triple the odds) might be misinterpreted as a probability 686 | 687 | - Now we're pretty far removed from familiar scales 688 | ] 689 | 690 | --- 691 | 692 | # Change in odds ratio: Cons 693 | 694 | .pull-left[ 695 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 696 | log.odds.p.half 697 | ``` 698 | ] 699 | 700 | .pull-right[ 701 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 702 | odds.ratio.p.half 703 | ``` 704 | ] 705 | 706 | --- 707 | 708 | # Change in odds ratio: Cons 709 | 710 | .pull-left[ 711 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 712 | log.odds.p.half + 713 | annotate("rect", ymin = -1.8, ymax = 1.3, xmin = 10.5, xmax = 11.5, 714 | fill = NA, color = "red", size = 2) + 715 | annotate("rect", ymin = -1.8, ymax = 1.3, xmin = 0.5, xmax = 1.5, 716 | fill = NA, color = "red", size = 2) 717 | ``` 718 | ] 719 | 720 | .pull-right[ 721 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 722 | odds.ratio.p.half + 723 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = 10.5, xmax = 11.5, 724 | fill = NA, color = "red", size = 2) + 725 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = 0.5, xmax = 1.5, 726 | fill = NA, color = "red", size = 2) 727 | ``` 728 | ] 729 | 730 | - The scale is expanded for positive effects and compressed for negative effects 731 | 732 | --- 733 | 734 | class: inverse, center, middle 735 | 736 | # Visualization family 2: 737 | 738 | # Presenting probabilities 739 | 740 | --- 741 | 742 | # Probabilities relative to a baseline 743 | 744 | - Problem with probabilities: change in percentage points depends on baseline starting value 745 | 746 | -- 747 | 748 | - We can choose an appropriate baseline probability, then compute the marginal effect of a predictor given that baseline 749 | 750 | -- 751 | 752 | - Options for baseline: 753 | 754 | -- 755 | 756 | - Model intercept 757 | 758 | -- 759 | 760 | - Observed outcome % in dataset (similar to intercept if continuous predictors are centered and other coefficients aren't too large) 761 | 762 | -- 763 | 764 | - Observed outcome % for a certain group (e.g., students with no tutoring) 765 | 766 | -- 767 | 768 | - Some % that's meaningful in context (e.g., 85% pass rate in typical years) 769 | 770 | --- 771 | 772 | # Probabilities relative to a baseline 773 | 774 | - Baseline probability: inverse logit of the intercept 775 | 776 | $$p_0 = \mbox{logit}^{-1}(\beta_0)$$ 777 | 778 | -- 779 | 780 | - Probability with discrete predictor $i$: inverse logit of intercept + predictor coefficient 781 | 782 | $$p_i = \mbox{logit}^{-1}(\beta_0 + \beta_i)$$ 783 | 784 | --- 785 | 786 | # Probabilities relative to a baseline 787 | 788 | ```{r probability_baseline, include = F, cache = F} 789 | knitr::read_chunk("visual_folder/probability_baseline.R") 790 | ``` 791 | 792 | ```{r probability-relative-to-some-baseline-no-arrows, fig.show = "hide"} 793 | ``` 794 | 795 | ```{r probability_baseline_plot, out.width = "100%"} 796 | prob.baseline.p = prob.baseline.p + 797 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 1), clip = "off") 798 | prob.baseline.p.full = prob.baseline.p + 799 | ggtitle(str_wrap(gsub("\\n", " ", prob.baseline.p$labels$title), 50)) 800 | prob.baseline.p.full 801 | ``` 802 | 803 | - (Uncertainty in intercept is not represented here) 804 | 805 | --- 806 | 807 | # Probabilities relative to a baseline: Pros 808 | 809 | .pull-left[ 810 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 811 | prob.baseline.p.half = prob.baseline.p + 812 | guides(color = guide_legend(nrow = 3)) + 813 | theme(legend.position = "bottom") 814 | prob.baseline.p.half 815 | ``` 816 | ] 817 | 818 | -- 819 | 820 | .pull-right[ 821 | - Familiar scale: probabilities, expressed as percentages 822 | 823 | {{content}} 824 | ] 825 | 826 | -- 827 | 828 | - Avoids the "percent change" formulation (common but misleading) 829 | 830 | --- 831 | 832 | # Probabilities relative to a baseline: Cons 833 | 834 | .pull-left[ 835 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 836 | prob.baseline.p.half + 837 | annotate("rect", ymin = invlogit(intercept) - 0.05, 838 | ymax = invlogit(intercept) + 0.05, xmin = 0, xmax = 12, 839 | fill = NA, color = "red", size = 2) 840 | ``` 841 | ] 842 | 843 | -- 844 | 845 | .pull-right[ 846 | - Have to choose a baseline; there may be no "good" choice 847 | 848 | {{content}} 849 | ] 850 | 851 | -- 852 | 853 | - Using the intercept as a baseline chooses reference categories for categorical variables 854 | 855 | --- 856 | 857 | # Probabilities relative to a baseline: Cons 858 | 859 | .pull-left[ 860 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 861 | prob.baseline.p.half + 862 | annotate("rect", ymin = invlogit(intercept) - 0.05, 863 | ymax = invlogit(intercept) + 0.05, xmin = 0, xmax = 12, 864 | fill = NA, color = "red", size = 2) 865 | ``` 866 | ] 867 | 868 | .pull-right[ 869 | - Have to choose a baseline; there may be no "good" choice 870 | 871 | - Using the intercept as a baseline chooses reference categories for categorical variables 872 | 873 | - Students who don't use Macs, don't wear glasses, etc. 874 | ] 875 | 876 | --- 877 | 878 | # Probabilities relative to a baseline: Cons 879 | 880 | .pull-left[ 881 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 882 | prob.baseline.p.half + 883 | annotate("rect", ymin = invlogit(intercept) - 0.05, 884 | ymax = invlogit(intercept) + 0.05, xmin = 0, xmax = 12, 885 | fill = NA, color = "red", size = 2) 886 | ``` 887 | ] 888 | 889 | .pull-right[ 890 | - Have to choose a baseline; there may be no "good" choice 891 | 892 | - Using the intercept as a baseline chooses reference categories for categorical variables 893 | 894 | - Students who don't use Macs, don't wear glasses, etc. 895 | 896 | - Not an appropriate choice for all datasets 897 | 898 | {{content}} 899 | ] 900 | 901 | -- 902 | 903 | - Doesn't show full range of possible effects at different baselines 904 | 905 | --- 906 | 907 | # Multiple baselines by group 908 | 909 | ```{r probability_group, include = F} 910 | knitr::read_chunk("visual_folder/probability_group.R") 911 | ``` 912 | 913 | ```{r probability-relative-to-some-baseline-and-group-no-arrows, fig.show = "hide"} 914 | ``` 915 | 916 | ```{r probability_group_plot, out.width = "100%"} 917 | prob.group.p = prob.group.p + 918 | coord_flip(xlim = c(0.9, 8.1), ylim = c(0, 1), clip = "off") 919 | prob.group.p.full = prob.group.p + 920 | ggtitle(str_wrap(gsub("\\n", " ", prob.group.p$labels$title), 50)) + 921 | theme(axis.text.y = element_text(size = 6)) 922 | prob.group.p.full 923 | ``` 924 | 925 | --- 926 | 927 | # Multiple baselines by group: Pros 928 | 929 | .pull-left[ 930 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 931 | prob.group.p.half = prob.group.p + 932 | guides(color = guide_legend(nrow = 3)) + 933 | theme(legend.position = "bottom") 934 | prob.group.p.half 935 | ``` 936 | ] 937 | 938 | -- 939 | 940 | .pull-right[ 941 | - Emphasizes that the baseline we show is a _choice_ 942 | ] 943 | 944 | --- 945 | 946 | # Multiple baselines by group: Pros 947 | 948 | .pull-left[ 949 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 950 | prob.group.p.half + 951 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 952 | fill = NA, color = "red", size = 1) 953 | ``` 954 | ] 955 | 956 | .pull-right[ 957 | - Emphasizes that the baseline we show is a _choice_ 958 | 959 | - Honors differences among groups 960 | ] 961 | 962 | --- 963 | 964 | # Multiple baselines by group: Pros 965 | 966 | .pull-left[ 967 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 968 | prob.group.p.half + 969 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 970 | fill = NA, color = "red", size = 1) 971 | ``` 972 | ] 973 | 974 | .pull-right[ 975 | - Emphasizes that the baseline we show is a _choice_ 976 | 977 | - Honors differences among groups 978 | 979 | - Effect of GPA is larger for fish owners than for dog owners 980 | ] 981 | 982 | --- 983 | 984 | # Multiple baselines by group: Pros 985 | 986 | .pull-left[ 987 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 988 | prob.group.p.half + 989 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 990 | fill = NA, color = "red", size = 1) 991 | ``` 992 | ] 993 | 994 | .pull-right[ 995 | - Emphasizes that the baseline we show is a _choice_ 996 | 997 | - Honors differences among groups 998 | 999 | - Effect of GPA is larger for fish owners than for dog owners 1000 | 1001 | - Here, this is purely because of fish owners' lower baseline 1002 | ] 1003 | 1004 | --- 1005 | 1006 | # Multiple baselines by group: Pros 1007 | 1008 | .pull-left[ 1009 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1010 | prob.group.p.half + 1011 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 1012 | fill = NA, color = "red", size = 1) 1013 | ``` 1014 | ] 1015 | 1016 | .pull-right[ 1017 | - Emphasizes that the baseline we show is a _choice_ 1018 | 1019 | - Honors differences among groups 1020 | 1021 | - Effect of GPA is larger for fish owners than for dog owners 1022 | 1023 | - Here, this is purely because of fish owners' lower baseline 1024 | 1025 | - But we could also show the effects of an interaction term in the model 1026 | 1027 | {{content}} 1028 | ] 1029 | 1030 | --- 1031 | 1032 | # Multiple baselines by group: Cons 1033 | 1034 | .pull-left[ 1035 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1036 | prob.group.p.half 1037 | ``` 1038 | ] 1039 | 1040 | -- 1041 | 1042 | .pull-right[ 1043 | - Still have to choose baselines by group 1044 | 1045 | {{content}} 1046 | ] 1047 | 1048 | -- 1049 | 1050 | - May suggest essentializing interpretations of groups 1051 | 1052 | {{content}} 1053 | 1054 | -- 1055 | 1056 | - Cluttered 1057 | 1058 | --- 1059 | 1060 | # Banana graphs 1061 | 1062 | - We can overcome the baseline-choosing problem by iterating across every baseline 1063 | 1064 | -- 1065 | 1066 | - For example: 1067 | 1068 | -- 1069 | 1070 | - Start with every possible probability of passing Balloon Animal-Making 201, from 0% to 100% (at sufficiently small intervals) 1071 | 1072 | -- 1073 | 1074 | - For each probability, add the effect of having a pet fish 1075 | 1076 | -- 1077 | 1078 | $$p_f = \mbox{logit}^{-1}(\mbox{logit}(p_0) + \beta_f)$$ 1079 | 1080 | --- 1081 | # Banana graphs 1082 | 1083 | ```{r banana_graphs, include = F} 1084 | knitr::read_chunk("visual_folder/banana_graphs.R") 1085 | ``` 1086 | 1087 | ```{r banana-graph, fig.show = "hide"} 1088 | banana.p = banana.p + 1089 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") 1090 | ``` 1091 | 1092 | ```{r out.width = "100%"} 1093 | banana.p 1094 | ``` 1095 | 1096 | --- 1097 | 1098 | # Banana graphs 1099 | 1100 | .pull-left[ 1101 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1102 | banana.p 1103 | ``` 1104 | ] 1105 | 1106 | -- 1107 | 1108 | .pull-right[ 1109 | - **x-axis:** baseline probability 1110 | 1111 | {{content}} 1112 | ] 1113 | 1114 | -- 1115 | 1116 | - **y-axis:** probability with effect of having a pet fish 1117 | 1118 | {{content}} 1119 | 1120 | -- 1121 | 1122 | - Solid line provides a reference (no effect) 1123 | 1124 | {{content}} 1125 | 1126 | -- 1127 | 1128 | - Positive effects above the line; negative effects below the line; no effect on the line 1129 | 1130 | --- 1131 | # Banana graphs 1132 | 1133 | ```{r out.width = "100%"} 1134 | banana.p + 1135 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") + 1136 | annotate("segment", x = 0.4, xend = 0.4, y = 0.4, 1137 | yend = invlogit(logit(0.4) + 1138 | coefs.df$est[coefs.df$parameter == "pet.typefish"]), 1139 | color = "black", size = 0.5, 1140 | arrow = arrow(type = "closed", length = unit(0.02, units = "npc"))) + 1141 | annotate("text", x = 0.1, y = 0.7, size = 2.90, hjust = 0, 1142 | label = str_wrap(paste("Some student who does not own a pet fish has a 40% chance of passing (x-axis value).", 1143 | " However, if that same student did own a pet fish,", 1144 | " their predicted probability of passing would be ", 1145 | round(invlogit(logit(0.4) + 1146 | coefs.df$est[coefs.df$parameter == "pet.typefish"]) * 100), 1147 | "% (y-axis value).", 1148 | sep = ""), 1149 | 30)) 1150 | ``` 1151 | 1152 | --- 1153 | 1154 | # Banana graphs 1155 | 1156 | ```{r banana-graph-multiple, fig.show = "hide"} 1157 | ``` 1158 | .center[ 1159 | ```{r out.width = "100%", fig.width = 9} 1160 | banana.multiple.p = banana.multiple.p + 1161 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") + 1162 | scale_fill_identity(guide = "legend", labels = c("Negative", "Not significant")) + 1163 | theme(legend.position = "right") + 1164 | labs(title ="Estimated relationship to probability of passing", 1165 | subtitle = "By pet type", 1166 | fill = "Relationship to\nprobability\nof passing") 1167 | banana.multiple.p 1168 | ``` 1169 | ] 1170 | 1171 | --- 1172 | 1173 | # Banana graphs: Pros 1174 | 1175 | .pull-left[ 1176 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1177 | banana.multiple.p 1178 | ``` 1179 | ] 1180 | 1181 | -- 1182 | 1183 | .pull-right[ 1184 | - Do not have to pick and choose a baseline 1185 | 1186 | {{content}} 1187 | ] 1188 | 1189 | -- 1190 | 1191 | - Show the whole range of predicted probabilities 1192 | 1193 | --- 1194 | 1195 | # Banana graphs: Cons 1196 | 1197 | .pull-left[ 1198 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1199 | banana.multiple.p 1200 | ``` 1201 | ] 1202 | 1203 | -- 1204 | 1205 | .pull-right[ 1206 | - Can take up quite a bit of space 1207 | 1208 | {{content}} 1209 | ] 1210 | 1211 | -- 1212 | 1213 | - May be initially difficult to understand 1214 | 1215 | {{content}} 1216 | 1217 | -- 1218 | 1219 | - Predictor variables are in separate graphs: hard to compare 1220 | 1221 | --- 1222 | 1223 | class: inverse, center, middle 1224 | 1225 | # Visualization family 3: 1226 | 1227 | # Counterfactual counts 1228 | 1229 | --- 1230 | 1231 | # Extra successes 1232 | 1233 | - Sometimes stakeholders are interested in **the number of times something happens (or doesn't happen)** 1234 | 1235 | -- 1236 | 1237 | - Example: stakeholders want to assess the impact of tutoring on pass rates in Balloon Animal-Making 201 1238 | 1239 | -- 1240 | 1241 | - They're interested not just in _whether_ tutoring helps students, but _how much_ it helps them 1242 | 1243 | -- 1244 | 1245 | - In our dataset, `r format(sum(df$tutoring), big.mark = ",")` students received tutoring; of those, `r format(sum(df$tutoring & df$passed), big.mark = ",")` passed the class 1246 | 1247 | -- 1248 | 1249 | - Suppose those students had _not_ received tutoring; in that case, how many would have passed? 1250 | 1251 | -- 1252 | 1253 | - In other words, how many "extra" passes did we get because of tutoring? 1254 | 1255 | --- 1256 | 1257 | # Extra successes 1258 | 1259 | - To get a point estimate: 1260 | 1261 | -- 1262 | 1263 | - Take all students who received tutoring 1264 | 1265 | -- 1266 | 1267 | - Set `tutoring` to `FALSE` instead of `TRUE` 1268 | 1269 | -- 1270 | 1271 | - Use the model to make (counterfactual) predictions for the revised dataset 1272 | 1273 | -- 1274 | 1275 | - Count predicted counterfactual passes; compare to the actual number of passes 1276 | 1277 | -- 1278 | 1279 | - We can get confidence intervals by simulating many sets of outcomes and aggregating over them. 1280 | 1281 | --- 1282 | 1283 | # Extra successes 1284 | 1285 | ```{r counterfactuals, include = F} 1286 | knitr::read_chunk("visual_folder/counterfactuals.R") 1287 | ``` 1288 | 1289 | ```{r extra-passes, fig.show = "hide"} 1290 | ``` 1291 | 1292 | ```{r extra_passes_plot, out.width = "100%"} 1293 | extra.p 1294 | ``` 1295 | 1296 | --- 1297 | 1298 | # Extra successes: Pros 1299 | 1300 | .pull-left[ 1301 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1302 | extra.p 1303 | ``` 1304 | ] 1305 | 1306 | -- 1307 | 1308 | .pull-right[ 1309 | - Counts have a straightforward interpretation 1310 | 1311 | {{content}} 1312 | ] 1313 | 1314 | -- 1315 | 1316 | - Natural baseline: account for other characteristics of your population (e.g., number of fish owners) 1317 | 1318 | --- 1319 | 1320 | # Extra successes: Cons 1321 | 1322 | .pull-left[ 1323 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1324 | extra.p 1325 | ``` 1326 | ] 1327 | 1328 | -- 1329 | 1330 | .pull-right[ 1331 | - "Number of simulations" may be hard to explain 1332 | 1333 | {{content}} 1334 | ] 1335 | 1336 | -- 1337 | 1338 | - Assumes that the counterfactual makes sense 1339 | 1340 | {{content}} 1341 | 1342 | -- 1343 | 1344 | - Strong causal interpretation 1345 | 1346 | --- 1347 | 1348 | # Extra successes by group 1349 | 1350 | - Your stakeholders may be interested in different effects by group 1351 | 1352 | -- 1353 | 1354 | - We can summarize counterfactuals for separate groups 1355 | 1356 | --- 1357 | 1358 | # Extra successes by group 1359 | 1360 | ```{r extra-passes-by-group, fig.show = "hide"} 1361 | ``` 1362 | 1363 | ```{r out.width = "100%"} 1364 | extra.group.p 1365 | ``` 1366 | 1367 | --- 1368 | 1369 | # Extra successes by group: Pros 1370 | 1371 | .pull-left[ 1372 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1373 | extra.group.p 1374 | ``` 1375 | ] 1376 | 1377 | -- 1378 | 1379 | .pull-right[ 1380 | - Avoids a scale with number of simulations; focus is on range of predictions 1381 | 1382 | {{content}} 1383 | ] 1384 | 1385 | -- 1386 | 1387 | - Shows differences by group 1388 | 1389 | {{content}} 1390 | 1391 | -- 1392 | 1393 | - Interaction terms in the model would be incorporated automatically 1394 | 1395 | --- 1396 | 1397 | # Extra successes by group: Cons 1398 | 1399 | .pull-left[ 1400 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1401 | extra.group.p 1402 | ``` 1403 | ] 1404 | 1405 | -- 1406 | 1407 | .pull-right[ 1408 | - Doesn't show how absolute numbers depend on group size 1409 | 1410 | {{content}} 1411 | ] 1412 | 1413 | -- 1414 | 1415 | - Tutoring actually has a _larger_ percentage point effect for fish owners (because of the lower baseline), but the group is small 1416 | 1417 | {{content}} 1418 | 1419 | -- 1420 | 1421 | - (Your audience may care about counts, percentages, or both) 1422 | 1423 | --- 1424 | 1425 | # Potential successes compared to group size 1426 | 1427 | ```{r potential-passes-by-group, fig.show = "hide"} 1428 | ``` 1429 | 1430 | ```{r out.width = "100%"} 1431 | potential.group.p 1432 | ``` 1433 | 1434 | --- 1435 | 1436 | # Potential successes compared to group size 1437 | 1438 | .pull-left[ 1439 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1440 | potential.group.p.half = potential.group.p + 1441 | theme(legend.position = "bottom") 1442 | potential.group.p.half 1443 | ``` 1444 | ] 1445 | 1446 | -- 1447 | 1448 | .pull-right[ 1449 | - Acknowledges different group sizes: puts absolute numbers in context 1450 | 1451 | {{content}} 1452 | ] 1453 | 1454 | -- 1455 | 1456 | - But small groups are squished at the bottom of the scale (hard to see) 1457 | 1458 | --- 1459 | 1460 | # Conclusion 1461 | 1462 | - There is no right or wrong way, only better and worse ways for a particular project, so get creative! 1463 | 1464 | -- 1465 | 1466 | - Knowing your stakeholders as well as the context and purpose of your research should be your guides to determine which visualization is most appropriate 1467 | 1468 | -- 1469 | 1470 | - Use colors, the layout, and annotations to your advantage 1471 | 1472 | -- 1473 | 1474 | - Share your ideas with others 1475 | 1476 | --- 1477 | 1478 | class: inverse, center, middle 1479 | 1480 | # Thank you! -------------------------------------------------------------------------------- /AIR_presentation/blinking_meme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikcaw/visualizing-logistic-regression/a97dafc9fab6cedce41b0a41ea88f1baece8474e/AIR_presentation/blinking_meme.jpg -------------------------------------------------------------------------------- /AIR_presentation/xaringan-themer-air.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------- 2 | * 3 | * !! This file was generated by xaringanthemer !! 4 | * 5 | * Changes made to this file directly will be overwritten 6 | * if you used xaringanthemer in your xaringan slides Rmd 7 | * 8 | * Issues or likes? 9 | * - https://github.com/gadenbuie/xaringanthemer 10 | * - https://www.garrickadenbuie.com 11 | * 12 | * Need help? Try: 13 | * - vignette(package = "xaringanthemer") 14 | * - ?xaringanthemer::write_xaringan_theme 15 | * - xaringan wiki: https://github.com/yihui/xaringan/wiki 16 | * - remarkjs wiki: https://github.com/gnab/remark/wiki 17 | * 18 | * ------------------------------------------------------- */ 19 | @import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic); 20 | @import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz); 21 | @import url(https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700); 22 | 23 | 24 | body { 25 | font-family: 'Droid Serif', 'Palatino Linotype', 'Book Antiqua', Palatino, 'Microsoft YaHei', 'Songti SC', serif; 26 | font-weight: normal; 27 | color: #000000; 28 | } 29 | h1, h2, h3 { 30 | font-family: 'Yanone Kaffeesatz'; 31 | font-weight: normal; 32 | background-color: #003865; 33 | color: #FFCD00; 34 | } 35 | .remark-slide-content { 36 | background-color: #FFFFFF; 37 | font-size: 20px; 38 | padding: 1em 4em 1em 4em; 39 | } 40 | .remark-slide-content h1 { 41 | font-size: 55px; 42 | padding: 10px 10px 0px 10px; 43 | } 44 | .remark-slide-content h2 { 45 | font-size: 45px; 46 | } 47 | .remark-slide-content h3 { 48 | font-size: 35px; 49 | } 50 | .remark-code, .remark-inline-code { 51 | font-family: 'Source Code Pro', 'Lucida Console', Monaco, monospace; 52 | } 53 | .remark-code { 54 | font-size: 0.9em; 55 | } 56 | .remark-inline-code { 57 | font-size: 1em; 58 | color: #00A8E1; 59 | 60 | 61 | } 62 | .remark-slide-number { 63 | color: #00A8E1; 64 | opacity: 1; 65 | font-size: 0.9em; 66 | } 67 | .inverse > .remark-slide-number { 68 | color: #FFCD00; 69 | opacity: 1; 70 | font-size: 0.9em; 71 | } 72 | strong{color:#00A8E1;} 73 | a, a > code { 74 | color: #FFCD00; 75 | text-decoration: none; 76 | } 77 | .footnote { 78 | 79 | position: absolute; 80 | bottom: 3em; 81 | padding-right: 4em; 82 | font-size: 0.9em; 83 | } 84 | .remark-code-line-highlighted { 85 | background-color: rgba(255,239,189,0.5); 86 | } 87 | .inverse { 88 | background-color: #003865; 89 | color: #FFCD00; 90 | text-shadow: 0 0 20px #333; 91 | } 92 | .inverse h1, .inverse h2, .inverse h3 { 93 | color: #FFCD00; 94 | } 95 | .title-slide .remark-slide-number { 96 | display: none; 97 | } 98 | /* Two-column layout */ 99 | .left-column { 100 | width: 20%; 101 | height: 92%; 102 | float: left; 103 | } 104 | .left-column h2, .left-column h3 { 105 | color: #00A8E199; 106 | } 107 | .left-column h2:last-of-type, .left-column h3:last-child { 108 | color: #00A8E1; 109 | } 110 | .right-column { 111 | width: 75%; 112 | float: right; 113 | padding-top: 1em; 114 | } 115 | .pull-left { 116 | float: left; 117 | width: 47%; 118 | } 119 | .pull-right { 120 | float: right; 121 | width: 47%; 122 | } 123 | .pull-right ~ * { 124 | clear: both; 125 | } 126 | img, video, iframe { 127 | max-width: 100%; 128 | } 129 | blockquote { 130 | border-left: solid 5px #FFEFBD80; 131 | padding-left: 1em; 132 | } 133 | .remark-slide table { 134 | margin: auto; 135 | border-top: 1px solid #666; 136 | border-bottom: 1px solid #666; 137 | } 138 | .remark-slide table thead th { border-bottom: 1px solid #ddd; } 139 | th, td { padding: 5px; } 140 | .remark-slide thead, .remark-slide tfoot, .remark-slide tr:nth-child(even) { background: #EEEEEE } 141 | table.dataTable tbody { 142 | background-color: #FFFFFF; 143 | color: #000000; 144 | } 145 | table.dataTable.display tbody tr.odd { 146 | background-color: #FFFFFF; 147 | } 148 | table.dataTable.display tbody tr.even { 149 | background-color: #EEEEEE; 150 | } 151 | table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { 152 | background-color: rgba(255, 255, 255, 0.5); 153 | } 154 | .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate { 155 | color: #000000; 156 | } 157 | .dataTables_wrapper .dataTables_paginate .paginate_button { 158 | color: #000000 !important; 159 | } 160 | 161 | @page { margin: 0; } 162 | @media print { 163 | .remark-slide-scaler { 164 | width: 100% !important; 165 | height: 100% !important; 166 | transform: scale(1) !important; 167 | top: 0 !important; 168 | left: 0 !important; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Intro.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Visualizing logistic regression results for non-technical audiences" 3 | author: "Abby Kaplan and Keiko Cawley" 4 | date: "9/20/2022" 5 | output: 6 | html_document: 7 | code_folding: hide 8 | toc: true 9 | toc_float: true 10 | --- 11 | 12 | ```{r setup, include = F, warning = F} 13 | library(dplyr) 14 | library(ggplot2) 15 | library(tidyverse) 16 | library(lme4) 17 | library(logitnorm) 18 | library(kableExtra) 19 | library(cowplot) 20 | library(grid) 21 | library(gridExtra) 22 | library(patchwork) 23 | library(knitr) 24 | theme_set(theme_bw()) 25 | opts_chunk$set(echo = T, message = F, warning = F, error = F, fig.retina = 3, 26 | fig.align = "center", fig.width = 6, fig.asp = 0.7) 27 | ``` 28 | 29 | # Introduction: Just tell me "the" effect 30 | 31 | Oftentimes stakeholders are interested in whether something has a positive or negative effect and if so, by how much. When we use a logistic regression to model a binary outcome, communicating these results is not so straightforward. To review, the formula for the logistic regression model is the following: 32 | 33 | $$ 34 | \mbox{logit}(p) = \log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 35 | $$ 36 | 37 | In the equation _p_ is the probability of the outcome, each of the *x~i~*s is a variable we are using to predict the outcome, and each of the _β_ coefficients represents some increase or decrease on the logistic regression curve associated with that variable. This equation tells us that each of the *β*s are in the log odds scale. This is precisely one of the reasons why it is difficult to convey the magnitude of effect for a logistic regression model. If everyone knew what a log odds was and how to convey measurements in the log odds scale, then interpreting logistic regression results would be a walk in the park. But for most of us, the log odds is not a unit of measurement we understand. A scale that we *do*, however, understand is the probability scale which ranges from 0 to 1. Fortunately, we can go from the log odds to probability scale by rearranging the equation above and solving for the probability _p_: 38 | 39 | $$ 40 | \begin{aligned} 41 | p & = \mbox{logit}^{-1}(\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n) \\ & \\ 42 | & = \frac{e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}}{1 + e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}} 43 | \end{aligned} 44 | $$ 45 | 46 | If we graph the equation we get a sigmoid-shaped curve: 47 | 48 | ```{r, echo = F, message = F, fig.height = 4, fig.width = 5, fig.align = "center"} 49 | ggplot() + 50 | stat_function(fun = function(x) (exp(x)/(1+(exp(x)))), color = "#009DD8") + 51 | scale_x_continuous(expression(beta[0]+beta[1]~x[1]~'+'~'...+'~beta[n]~x[n]), 52 | limits = c(-6, 6), breaks = seq(-6, 6, 2)) + 53 | scale_y_continuous("probability (p)", limits = c(0, 1)) + 54 | annotate("segment", x = -3, xend = -2, y = invlogit(-3), yend = invlogit(-3), 55 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 56 | annotate("segment", x = -2, xend = -2, y = invlogit(-3), yend = invlogit(-2), 57 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 58 | annotate("text", x = -3.5, y = 0.3, 59 | label = str_wrap("A 1-unit change here leads to a small change in probability", 60 | 20)) + 61 | annotate("segment", x = 0, xend = 1, y = invlogit(0), yend = invlogit(0), 62 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 63 | annotate("segment", x = 1, xend = 1, y = invlogit(0), yend = invlogit(1), 64 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 65 | annotate("text", x = 3, y = 0.6, 66 | label = str_wrap("A 1-unit change here leads to a large change in probability", 67 | 20)) + 68 | ylim(0, 1) + 69 | xlim(-5, 5) + 70 | ylab("probability (p)") + 71 | xlab(label = expression(z~'='~beta[0]+beta[1]~X[1]~'+'~'...+'~beta[n]~X[n])) 72 | ``` 73 | 74 | The slope of the curve is not constant: a one-unit increase for different values of _z_ can result in vastly different probabilities _p_. Where the slope of the curve is relatively flat, for example from _z_ = -5 to _z_ = -2.5, the difference in probability from a one-unit increase is very small. On the other hand, where the slope curve is steep, for example from _z_ = 0 to _z_ = 2.5, the difference in probability from a one-unit increase is large. 75 | 76 | The non-linear nature of the curve means that **the magnitude of effect in percentage points varies**. If the effect varies, then what is "the" effect that we report, and particularly to a stakeholder who does not know what a logistic regression is? In this discussion we will explore various visualization options to present logistic regression results to non-technical audiences, and the pros and cons of each option. We will also discuss in which situations you might choose one visualization over another. 77 | 78 | # Sample dataset and model 79 | 80 | ```{r fit_model, include = F} 81 | knitr::read_chunk("R_code/fit_model.R") 82 | ``` 83 | 84 | ### Dataset 85 | 86 | Our simulated dataset describes students who took Balloon Animal-Making 201 at University Imaginary. It is available in [`data/course_outcomes.csv`](https://github.com/keikcaw/visualizing-logistic-regression/blob/main/data/course_outcomes.csv) (included in the GitHub repository). 87 | 88 | ```{r load-data, class.source = "fold-show"} 89 | ``` 90 | 91 | The dataset contains the following variables: 92 | 93 | ```{r dataset_summary_table, echo = F} 94 | table <- data.frame( 95 | Variable = c("Mac user", "Wear glasses", "Pet type", "Favorite color", "Prior undergraduate GPA", "Height", "Went to tutoring", "Passed"), 96 | Possible.Responses = c("TRUE/FALSE", "TRUE/FALSE", "dog, cat, fish, none", "blue, red, green, orange", "0.0-4.0", "54-77 inches", "TRUE/FALSE", "TRUE/FALSE"), 97 | Variable.Type = c("binary", "binary", "categorical", "categorical", "continuous", "continuous", "binary", "binary") 98 | ) 99 | table %>% 100 | kbl(col.names = gsub("[.]", " ", names(table))) %>% 101 | kable_styling(bootstrap_options = c("striped", "hover"), full_width = F) 102 | ``` 103 | 104 | We centered and standardized the continuous variables: 105 | 106 | ```{r prepare-data-continuous, class.source = "fold-show"} 107 | ``` 108 | 109 | We set the reference levels of the categorical variables (pet type and favorite color) to no pet and blue, respectively: 110 | 111 | ```{r prepare-data-categorical, class.source = "fold-show"} 112 | ``` 113 | 114 | ### Model 115 | 116 | We built a logistic regression model to determine what variables are associated with a student passing the Balloon Animal-Making course. 117 | 118 | ```{r model, class.source = "fold-show"} 119 | ``` 120 | 121 | We pulled out the model coefficients and created a dataframe. This dataframe will be used throughout our visualization-exploration journey. 122 | 123 | ```{r get-coefficients, class.source = "fold-show"} 124 | ``` 125 | 126 | ### Colors 127 | 128 | The code below uses a few named colors for consistency across graphs: 129 | 130 | ```{r colors, include = F} 131 | knitr::read_chunk("visual_folder/colors.R") 132 | ``` 133 | 134 | ```{r color-palette, class.source = "fold-show"} 135 | ``` 136 | 137 | ```{r echo = F, fig.asp = 0.3} 138 | data.frame(color.name = paste(c("good", "neutral", "bad"), "color", sep = "."), 139 | hex.code = c(good.color, neutral.color, bad.color)) %>% 140 | mutate(color.name = fct_relevel(color.name, "good.color", 141 | "neutral.color")) %>% 142 | ggplot(aes(x = color.name, y = 1, color = hex.code)) + 143 | geom_point(size = 30) + 144 | scale_color_identity() + 145 | theme_void() + 146 | theme(axis.text.x = element_text(size = 14, margin = margin(t = 0, b = 5))) 147 | ``` 148 | 149 | ### Disclaimer: causality 150 | 151 | Several of the visualizations below strongly imply a causal effect of some predictor on the outcome. However, this discussion is not about causality, and we will not discuss how to robustly evaluate causality. Although the logistic regression model estimates the values of the predictor variables, whether a causal relationship actually exists between the predictor and outcome variable requires careful consideration and domain knowledge surrounding the research question. You should use your judgment when considering these visualizations: if your data doesn't justify a causal interpretation, don't use them! 152 | 153 | # Presenting model coefficients 154 | 155 | As the researcher, perhaps the easiest visualization we can create from the logistic regression analysis is a plot of the raw outputs of the predictors estimated by the model. In this section we will review visualizations based on the raw coefficients or simple functions thereof. 156 | 157 | ### Log odds 158 | 159 | ```{r log_odds, include = F, cache = F} 160 | knitr::read_chunk("visual_folder/log_odds.R") 161 | ``` 162 | 163 | Oftentimes in papers we see a summary of the model's raw outputs in a table, like this: 164 | 165 | ```{r coefficient_table, echo = F} 166 | coefs.df %>% 167 | dplyr::select(-parameter) %>% 168 | kbl(col.names = c("Parameter", "Estimate", "Standard error", "z", "p")) %>% 169 | kable_styling(bootstrap_options = c("striped", "hover"), full_width = F) 170 | ``` 171 | 172 | Although this table may be appropriate for technical audiences who are familiar with logistic regression, but non-technical audiences will not understand the meaning behind each of the columns. Furthermore, the numbers in the table can be taxing on the eyes making it difficult to absorb insights from your model. We can aid the audience by swapping out the table with a caterpillar plot of the predictors. 173 | 174 | ```{r change-in-log-odds} 175 | ``` 176 | 177 | ```{r change_in_log_odds_plot, echo = F} 178 | log.odds.p 179 | ``` 180 | 181 | This plot clearly presents all the predictors used in the model; catalogs which effects are positive, negative, or of no effect; and contains confidence intervals to convey the uncertainty in our estimates. You could argue that a table could do these things as well: color the cells of the table, add columns for the confidence intervals, and order the cells. Therefore, the characteristic which makes this plot more visually appealing than a table is that the numbers are isolated in one place: the x-axis. However, using the raw outputs from the logistic regression means that the magnitude of effect is in the log odds scale. This visual is perfect for audiences who know exactly what a log odds is, and problematic for non-technical audiences who do not. The visual can leave your non-technical audience member wondering what a 0.4 change in the log odds even means. Is the difference between a change in the log odds of 0.4 and 0.8 big or small? If the scale doesn't mean anything to your audience member then it is your duty as the researcher to either provide an explanation or make an adjustment. 182 | 183 | ### Secret log odds 184 | 185 | Rather than trying to have your audience understand log odds, the simplest adjustment you could make is to relabel the x-axis. This second visual is exactly the same as the fist visual, with the only difference being in the x-axis. Instead of the x-axis showing the log odds scale explicitly, it simply indicates whether the chance of passing is higher, the same, or lower. 186 | 187 | ```{r change-in-log-odds-adjusted-axis} 188 | ``` 189 | ```{r change_in_log_odds_adjusted_axis_plot, echo = F} 190 | secret.log.odds.p 191 | ``` 192 | 193 | This graph is perfect if your audience is only interested in knowing which variables (if any) are related to the outcome, and what the sign of each relationship is. Within limits, it can also show which variables are more strongly related to the outcome than others (depending on how continuous predictors are scaled). However, relabeling the axis this way means that there are bands of different lengths but no unit of measurement to describe their absolute effect size. Therefore, just like the first graph, we run into the same issue: the non-technical audience member still will not get a sense of the overall magnitude of effect. This is problematic because say, for example, your stakeholder asked you to evaluate whether offering tutoring sessions to students in Balloon Animal-Making 201 is helping students pass the class. The effect of the tutoring intervention is positive, but this graph doesn't indicate what that means in practical terms: does it increase a student's chance of passing by 5%? 90%? Your stakeholder may deem that a 5% increase is not a high enough increase to justify funding the tutoring program. 194 | 195 | ### Odds ratios 196 | 197 | ```{r odds_ratio, include = F, cache = F} 198 | knitr::read_chunk("visual_folder/odds.R") 199 | ``` 200 | 201 | The log odds scale is hard to interpret directly, but your audience may be more familiar with the "odds" part of log odds. The quantity $\frac{p}{1 - p}$ is another way of expressing ideas like "3-to-1 odds" or "2-to-5 odds". Can we exponentiate the log odds to get something more interpretable? 202 | 203 | Sort of, but there are two non-trivial obstacles. The first is that your audience is likely to be more familiar with odds expressed as integer ratios ("3-to-1" or "2-to-5") rather than "3" or "0.4". Your model is unlikely to produce odds that are rational numbers for small integers, so you'll need to either explain your unusual-looking odds or convert them to approximations (for example, "roughly 1-to-3 odds" for a value of 0.35). 204 | 205 | The second, and more serious, problem is that the coefficients of your model (which are probably what your audience cares most about) don't represent odds directly; they represent _changes_ in odds. Each coefficient represents an additive change on the log odds scale; when we exponentiate to get odds, each coefficient represents a multiplicative change. That is, for each _β~i~_, a one-unit increase in _x~i~_ multiplies the odds of the outcome by _e^β~i~^_. To put it yet another way, the coefficients represent a change in the _odds ratio_. 206 | 207 | $$ 208 | \begin{aligned} 209 | e^{\beta_i} & = \frac{e^{\beta_i}e^{\beta_ix_i}}{e^{\beta_ix_i}} \\ \\ 210 | & = \frac{e^{\beta_ix_i + \beta_i}}{e^{\beta_ix_i}} \\ \\ 211 | & = \frac{e^{\beta_i(x_i + 1)}}{e^{\beta_ix_i}} \\ \\ 212 | & = \frac{e^{\beta_i(x_i + 1)} \cdot e^{\beta_0 + \beta_1x_1 + \ldots + \beta_{i - 1}x_{i - 1} + \beta_{i + 1}x_{i + 1} \ldots \beta_nx_n}}{e^{\beta_ix_i} \cdot e^{\beta_0 + \beta_1x_1 + \ldots + \beta_{i - 1}x_{i - 1} + \beta_{i + 1}x_{i + 1} \ldots \beta_nx_n}} \\ \\ 213 | & = \frac{e^{\log(\mbox{odds for } x_i + 1)}}{e^{\log(\mbox{odds for } x_i)}} \\ \\ 214 | & = \frac{\mbox{odds for } x_i + 1}{\mbox{odds for } x_i} 215 | \end{aligned} 216 | $$ 217 | 218 | We can plot exponentiated coefficients just like raw coefficients. 219 | 220 | ```{r odds-ratio-adjusted-axis} 221 | ``` 222 | ```{r odds_ratio_adjusted_axis_plot, echo = F} 223 | odds.ratio.p 224 | ``` 225 | 226 | Changes in odds ratios may be a bit easier to describe for your audience than changes in log odds. (For example, tripling the odds ratio is like going from 3-to-1 odds to 9-to-1 odds, or from 1-to-3 odds to an even chance.) But we're still pretty far removed from the kinds of scales your audience will be most familiar with, like percentages. Worse, there's a danger that the percent change in log odds might be misinterpreted as the absolute probability of the outcome (or the change in its probability), which is not what these plots represent at all. Finally, when we represent the coefficients as changes in odds ratios, we expand the scale for coefficients with a positive effect and compress it for coefficients with a negative effect. (The odds ratio graph suggests that a `r round(sd(df$prior.gpa), 1)`-point increase in prior GPA has a much larger effect on passing than having a pet fish, while the graph of log odds suggests that the effects have approximately the same magnitude.) 227 | 228 | # Presenting probabilities 229 | 230 | ### Probability relative to some baseline 231 | 232 | ```{r probability_baseline, include = F} 233 | knitr::read_chunk("visual_folder/probability_baseline.R") 234 | ``` 235 | 236 | Your audience may not be familiar with log odds or odds ratios, but they are certainly familiar with probabilities. We've already discussed the primary obstacle to translating logistic regression coefficients into probabilities: the change in percentage points depends on the baseline starting value. But if we can choose an appropriate baseline probability, we can present our model coefficients on a probability scale after all. 237 | 238 | One approach is to use the overall intercept of the model as a baseline. When we take the inverse logit of the intercept, we get the probability of passing for a student with average prior GPA, average height, and the baseline value of each categorical variable -- not a Mac user, doesn't wear glasses, has no pet, favorite color is blue, and didn't go to tutoring. This baseline probability of passing, as it turns out, is `r round(invlogit(coefs.df$est[coefs.df$parameter == "(Intercept)"]) * 100)`%. We can then add each coefficient individually to the intercept to discover the predicted probability of passing for a baseline student with one characteristic changed: 239 | 240 | ```{r probability-relative-to-some-baseline-no-arrows} 241 | ``` 242 | ```{r probability_baseline_plot, echo = F} 243 | prob.baseline.p 244 | ``` 245 | 246 | This graph includes confidence intervals around the predicted probabilities, just like the confidence intervals around the coefficients in the earlier graphs. Alternatively, we can use arrows to emphasize that we're showing predicted differences from a baseline: 247 | 248 | ```{r probability-relative-to-some-baseline-with-arrows} 249 | ``` 250 | ```{r probability_baseline_arrows_plot, echo = F} 251 | prob.baseline.arrows.p 252 | ``` 253 | 254 | The biggest advantage of this approach is that it uses a scale that your audience is already very familiar with: probabilities (expressed as percentages). Moreover, it avoids a common, but misleading, way of presenting changes in probabilities: the "percent change" formulation. If I tell you that tutoring doubles a student's chances of passing, you don't know whether it raises the student's chances from 1% to 2% or from 40% to 80% -- and that difference probably matters to you! Instead, it presents the "before" and "after" probabilities simultaneously, which is much more helpful context for the audience. 255 | 256 | The biggest difficulty with this approach is choosing an appropriate baseline. The idea of representing an "average" student seems reasonable enough, although the process for doing so depends on how the model is specified. In our model, the intercept represents a student with average values for the continuous predictors because we standardized those predictors; if we hadn't, the intercept would be quite different and would represent a student with a prior GPA and height of 0. Such a baseline wouldn't make any sense; in that case, we would want to make adjustments (for example, by adding the mean prior GPA and height back to the intercept, multiplied by their respective coefficients). 257 | 258 | More seriously, with respect to the categorical predictors, our baseline represents a student who is _not_ actually all that average: it's a student who is not a Mac user, doesn't wear glasses, etc. All of our baseline categories are the most frequent values in the dataset, but it's nevertheless the case that students with _all_ of the most frequent values are a small minority (only `r df %>% mutate(all.freq = as.numeric(!mac & !glasses & pet.type == "none" & favorite.color == "blue" & !tutoring) * 100) %>% pull(all.freq) %>% mean() %>% round()`% of the dataset). Whether this is an acceptable baseline depends on your particular situation. 259 | 260 | Another approach would be to take the raw pass rate from the overall dataset and use _that_ as the baseline. (To get probabilities for each predictor, we would take the logit of the raw pass rate, add the appropriate coefficient, and take inverse logit to get back to a probability.) Or you could choose a probability that is meaningful for your particular application (for example, maybe your stakeholders are especially interested in students who are "on the edge", and so you might choose 50% as a baseline). What all this discussion shows is that presenting your model as changes in probabilities doesn't eliminate the need to explain the visualization to your stakeholders. You may not have to explain the probability scale, but you will definitely need to explain how and why you chose the baseline you did. 261 | 262 | ### Multiple baselines by group 263 | 264 | ```{r probability_group, include = F} 265 | knitr::read_chunk("visual_folder/probability_group.R") 266 | ``` 267 | 268 | If there's no single appropriate baseline probability for your dataset, you might choose to make multiple plots, one for each of several baselines. In the example below, we show four baselines, one for each type of pet a student might have. The baseline for each group is the overall model intercept plus the coefficient for that type of pet. 269 | 270 | ```{r probability-relative-to-some-baseline-and-group-no-arrows} 271 | ``` 272 | ```{r probability_group_plot, echo = F, fig.width = 6.5, fig.asp = 1} 273 | prob.group.p 274 | ``` 275 | 276 | As before, we can use arrows instead of confidence intervals: 277 | 278 | ```{r probability-relative-to-some-baseline-and-group-with-arrows} 279 | ``` 280 | 281 | ```{r probability_group_arrows_plot, echo = F, fig.width = 6.5, fig.asp = 1} 282 | prob.group.arrows.p 283 | ``` 284 | 285 | This approach is more cluttered than a single graph. But, if that's not a deal-breaker for you, it has several advantages. First, it emphasizes that the baseline we show is a _choice_, and that different students have different baselines. 286 | 287 | Second, this approach explicitly shows how the effect of a given predictor varies depending on the baseline. In the example above, prior GPA is associated with a larger percentage point change for students who have a fish than for other types of students, because students with fish start at a different baseline. 288 | 289 | Finally, this approach allows you to show different effects by group. In our example, the larger effect for students with fish is purely a function of their lower baseline. But we could easily imagine a model with true _interactions_ between pet type and other predictors. (For example, maybe tutoring is more effective for dog owners than for cat owners.) In that case, we would just add the interaction term when computing the probability for the relevant predictor. 290 | 291 | ### Banana graphs 292 | 293 | ```{r banana_graphs, include = F} 294 | knitr::read_chunk("visual_folder/banana_graphs.R") 295 | ``` 296 | 297 | Even if we present multiple groups, we still have to choose a (possibly arbitrary) baseline for each group. We can overcome this picking-and-choosing problem by iterating across every baseline. For example, we can calculate the 0% to 100% predicted probability of passing Balloon Animal-Making 201 for a student who does not own a pet fish, and use those baseline values to compute the predicted probability of a student who does own a pet fish and plot these values on a graph. The x-axis value for each point on the curve is the baseline probability, i.e., the probability that the student does not own a pet fish. The y-axis value for each point on the curve is the probability increase or decrease from the baseline, i.e., the probability that the student does own a pet fish. Due to the shape of the curve, we call this graph the banana graph. We also include a solid line which goes diagonally across the middle of the graph as a reference line. When the banana-shaped curve is: 298 | 299 | * **Above the solid line**: the predictor variable has a higher predicted probability of passing than its baseline 300 | * **On the solid line**: the predictor variable has the same predicted probability of passing as its baseline 301 | * **Below the solid line**: the predictor variable has a lower predicted probability of passing than its baseline 302 | 303 | In the figure below, you can see that the banana-shaped curve is below the solid line, meaning, students who own a pet fish are less likely to pass than students who do not own a pet fish. 304 | 305 | ```{r create-banana-graph} 306 | ``` 307 | 308 | ```{r banana-graph} 309 | ``` 310 | 311 | ```{r banana_graph_plot, echo = F} 312 | banana.p 313 | ``` 314 | 315 | However, these banana graphs can be difficult for audience members to understand and tricky to explain. To overcome this issue, we suggest that you begin by presenting one banana graph enlarged on the page, choosing one example point, and annotating the graph with how the example point should be interpreted (as shown below). If your audience is unfamiliar with these graphs it can be overwhelming, and their eyes might gloss over the figures if you begin filling the page with these graphs. Starting with one banana graph and enlarging it prevents your audiences' eyes from wandering and becoming overwhelmed. The annotation provides a concrete example of how the audience should be interpreting each point on the graph. 316 | 317 | ```{r banana_graph_highlighted_plot, echo = F} 318 | banana.p + 319 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") + 320 | annotate("segment", x = 0.4, xend = 0.4, y = 0.4, 321 | yend = invlogit(logit(0.4) + 322 | coefs.df$est[coefs.df$parameter == "pet.typefish"]), 323 | color = "black", size = 0.5, 324 | arrow = arrow(type = "closed", length = unit(0.02, units = "npc"))) + 325 | annotate("text", x = 0.1, y = 0.7, size = 2.90, hjust = 0, 326 | label = str_wrap(paste("Some student who does not own a pet fish has a 40% chance of passing (x-axis value).", 327 | " However, if that same student did own a pet fish,", 328 | " their predicted probability of passing would be ", 329 | round(invlogit(logit(0.4) + 330 | coefs.df$est[coefs.df$parameter == "pet.typefish"]) * 100), 331 | "% (y-axis value).", 332 | sep = ""), 333 | 30)) 334 | ``` 335 | 336 | Once you've helped the audience understand one banana graph, you can then go on to present multiple banana graphs, like so: 337 | 338 | ```{r banana-graph-multiple} 339 | ``` 340 | ```{r banana_graph_multiple_plot, echo = F, fig.asp = 1.2} 341 | banana.multiple.p 342 | ``` 343 | 344 | These graphs are perfect for showing the whole range of predicted probabilities. However, the downside to these graphs is, if you're planning on showing the effect of multiple variables, they can take up a great deal of real estate on your report. The banana graphs require one graph per predictor variable. In the case that you must present the effect of many predictor variables, you may overwhelm your audience by the sheer volume of graphs. Additionally, the caterpillar plots in the previous examples could show all the predictors and their effect size in one graph, which means one can easily compare values between any two predictors in one caterpillar plot. However, with the banana graphs, you audience's eyes have to dart from one graph to the next to compare any two predictor variables. Another limitation to this visual is that, if it is your audience's first time seeing these banana graphs, then it may be difficult for them to understand. Although we discussed a design layout to overcome this issue, it is still important to think about whether there is a better visual which does not require you to go to certain lengths to explain what the graph is trying to convey. 345 | 346 | # Presenting counterfactual counts 347 | 348 | ```{r counterfactuals, include = F} 349 | knitr::read_chunk("visual_folder/counterfactuals.R") 350 | ``` 351 | 352 | Sometimes stakeholders are interested in a measure that's even more basic than probabilities: the number of times something happens (or doesn't happen). 353 | 354 | For example, suppose your stakeholders want to use your analysis to assess the impact of tutoring on pass rates in Balloon Animal-Making 201. Since the tutoring program costs money, they're interested not just in _whether_ tutoring helps students, but _how much_ it helps them. In other words, is the program worth it, or could the money be used more effectively elsewhere? A very direct way to assess the real-world impact of the program is to estimate a literal count: the number of extra students who passed because of tutoring. 355 | 356 | ### Extra successes 357 | 358 | Here's one way to approach this idea. In our dataset, `r format(sum(df$tutoring), big.mark = ",")` students received tutoring; of those, `r format(sum(df$tutoring & df$passed), big.mark = ",")` passed the class. Suppose those students had _not_ received tutoring; in that case, how many students do we think would have passed? In other words, how many "extra" passes did we get because of tutoring? 359 | 360 | We can get a simple point estimate by using our model to predict outcomes for the subset of our data where students received tutoring, but with the `tutoring` predictor set to `FALSE` instead of `TRUE` -- in other words, by running a set of counterfactual predictions. But a point estimate doesn't convey the amount of uncertainty around our estimate; we can get confidence intervals by simulating many sets of outcomes and aggregating over them, as follows: 361 | 362 | 1. Take all observations in the dataset for students who received tutoring. 363 | 2. Set the value of the `tutoring` predictor to `FALSE` for all students. 364 | 2. For each of (5,000, or some other suitably large number) simulations: 365 | 1. **Account for uncertainty in parameter estimates:** Sample a value for each parameter in the model. We took a sample from the normal distribution with its mean set to the estimated parameter value and its standard deviation set to the standard error. (Ideally, our sampling procedure would also take into account the correlations among the fitted parameters. We haven't done this here; the correlations in this particular model are all quite small.) 366 | 2. Compute the predicted probability of passing for each student using these parameter values. 367 | 2. **Account for uncertainty in outcomes:** Randomly assign each prediction to "pass" or "fail", weighted by the predicted probability of passing. 368 | 2. Sum the total number of students predicted to pass. Subtract this total from the number of students who actually passed. This is the predicted number of "extra" passes. 369 | 2. Summarize the distribution of predicted number of extra passing students over all the simulations. 370 | 371 | The histogram below shows the results of 5,000 simulations like this for our dataset. Our model estimates that tutoring did indeed increase the number of students who passed -- probably by somewhere around 50-150 students. 372 | 373 | ```{r extra-passes} 374 | ``` 375 | 376 | ```{r extra_passes_plot, echo = F} 377 | extra.p 378 | ``` 379 | 380 | If your stakeholders care about the magnitude of an effect in terms of counts, this approach is clear and straightforward; it directly answers the question stakeholders are asking. Its primary drawback is that, even more than other visualizations we've explored, it strongly implies a causal relationship between the predictor and the outcome: we're claiming that the tutoring _caused_ some students to pass who otherwise wouldn't have. Moreover, this approach assumes that the counterfactual makes sense. This is fine for the tutoring predictor; it's reasonable to ask what would have happened if a student hadn't gone to tutoring. But it makes much less sense for other predictors; for example, what does it mean to ask what would have happened if a student's favorite color were red instead of blue? 381 | 382 | Using a histogram to summarize the simulations may not be the best choice if your audience is likely to be distracted by the whole idea of using simulation to estimate uncertainty. The meaning of the y-axis isn't straightforward and will require some explanation. 383 | 384 | ### Extra successes by group 385 | 386 | Just as we did with the probability-based approaches above, we can summarize counterfactuals by group for a more fine-grained view of our model's predictions. The following graph shows the number of extra passing students by pet type. To save space, we've flattened the histograms into a caterpillar plot; this has the additional advantage of avoiding a scale that shows the number of simulations, and instead focusing on the range of predictions (which is what we wanted in the first place). 387 | 388 | ```{r extra-passes-by-group} 389 | ``` 390 | ```{r extra_passes_group_plot, echo = F} 391 | extra.group.p 392 | ``` 393 | 394 | Splitting up the estimates by group has advantages similar to the ones described above for presenting probabilities by group: it shows how the effect varies by group, and if our model had relevant interactions, we would be able to include those. 395 | 396 | It's worth emphasizing that the _kinds_ of differences we see here are different from those we saw in the probability graphs. For example, recall that we saw that tutoring gives a larger percentage point boost to students with fish, because those students started out with a lower baseline probability of passing. But this graph shows that the absolute _number_ of extra passing students with fish is _smaller_ than for other groups; this is because there simply aren't that many students with fish in the first place. Your context will determine which kind of effect is most relevant. Do your stakeholders want to know absolute numbers, so that they can avoid spending resources that won't actually benefit many people? Or do they want to understand whether a particular intervention disproportionately benefits (or harms) certain groups, even if those groups are small? 397 | 398 | ### Potential successes compared to group size 399 | 400 | There's an alternative way to present counterfactuals that attempts to show _both_ the effect size for each group _and_ the overall size of that group. For this example, we'll switch the direction of the counterfactual: instead of predicting how many tutored students passed who otherwise wouldn't have, we're going to predict how many _untutored_ students _would have_ passed if they had received tutoring. (Again, we're making a strong causal claim, which may not be appropriate for your model!) 401 | 402 | The graph below shows, for each pet type, the number of untutored students with that kind of pet who _actually_ passed the class (red diamonds). It also shows the estimated number of untutored students who _would have_ passed if they had received tutoring (black dots and lines). We can see clearly that our model predicts a benefit from tutoring for all students except dog owners; it also makes clear that the groups have substantially different sizes. 403 | 404 | ```{r potential-passes-by-group} 405 | ``` 406 | ```{r potential_passes_group_plot, echo = F} 407 | potential.group.p 408 | ``` 409 | 410 | This approach is a step towards acknowledging different group sizes, and it also helps put the absolute numbers in context. (50 extra passes is extremely impressive if the baseline was 50, but less so if the baseline was 10,000.) But note that it doesn't convey the size of the _whole_ group, only of the number of passing students in the group. For example, the graph doesn't explain whether the number of passing students with fish is small because there aren't many of them in the first place, or because fish owners are more likely to fail. (In this case it's both, but that won't always be true.) In addition, if you have groups of vastly unequal sizes, then the effects for smaller groups will be squashed at the bottom of the scale and difficult to see. 411 | 412 | # Conclusion 413 | 414 | For logistic regression models, just as there is not *one* straightforward effect we can report to our non-technical audience members, there is also not *one* visualization for these models that can (should) be presented to them. There are visualizations which are more or less appropriate for different situation. Knowing our stakeholders as well as the context and purpose of our research should be our guides to determine which visualization is most appropriate. Although it's easy to get caught up in presenting the model's results for the sake of presenting the model's results, it's important to recall that our results mean something and they should mean something to our audiences too. 415 | 416 | We hope that this guide can serve as a springboard for you to create other visualizations for presenting logistic regression results. There is no right or wrong way, only better and worse ways for a particular project, so, get creative! Use colors, the layout, and annotations to your advantage, and share your ideas with others. Good luck! 417 | 418 | -------------------------------------------------------------------------------- /Presentation_files/xaringan-themer.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------- 2 | * 3 | * !! This file was generated by xaringanthemer !! 4 | * 5 | * Changes made to this file directly will be overwritten 6 | * if you used xaringanthemer in your xaringan slides Rmd 7 | * 8 | * Issues or likes? 9 | * - https://github.com/gadenbuie/xaringanthemer 10 | * - https://www.garrickadenbuie.com 11 | * 12 | * Need help? Try: 13 | * - vignette(package = "xaringanthemer") 14 | * - ?xaringanthemer::write_xaringan_theme 15 | * - xaringan wiki: https://github.com/yihui/xaringan/wiki 16 | * - remarkjs wiki: https://github.com/gnab/remark/wiki 17 | * 18 | * ------------------------------------------------------- */ 19 | @import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic); 20 | @import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz); 21 | @import url(https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700); 22 | 23 | 24 | body { 25 | font-family: 'Droid Serif', 'Palatino Linotype', 'Book Antiqua', Palatino, 'Microsoft YaHei', 'Songti SC', serif; 26 | font-weight: normal; 27 | color: #000000; 28 | } 29 | h1, h2, h3 { 30 | font-family: 'Yanone Kaffeesatz'; 31 | font-weight: normal; 32 | } 33 | .remark-slide-content { 34 | background-color: #FFFFFF; 35 | font-size: 20px; 36 | 37 | 38 | 39 | padding: 1em 4em 1em 4em; 40 | } 41 | .remark-slide-content h1 { 42 | font-size: 55px; 43 | } 44 | .remark-slide-content h2 { 45 | font-size: 45px; 46 | } 47 | .remark-slide-content h3 { 48 | font-size: 35px; 49 | } 50 | .remark-code, .remark-inline-code { 51 | font-family: 'Source Code Pro', 'Lucida Console', Monaco, monospace; 52 | } 53 | .remark-code { 54 | font-size: 0.9em; 55 | } 56 | .remark-inline-code { 57 | font-size: 1em; 58 | color: #00A8E1; 59 | 60 | 61 | } 62 | .remark-slide-number { 63 | color: #00A8E1; 64 | opacity: 1; 65 | font-size: 0.9em; 66 | } 67 | strong{color:#00A8E1;} 68 | a, a > code { 69 | color: #00A8E1; 70 | text-decoration: none; 71 | } 72 | .footnote { 73 | 74 | position: absolute; 75 | bottom: 3em; 76 | padding-right: 4em; 77 | font-size: 0.9em; 78 | } 79 | .remark-code-line-highlighted { 80 | background-color: rgba(255,255,0,0.5); 81 | } 82 | .inverse { 83 | background-color: #272822; 84 | color: #d6d6d6; 85 | text-shadow: 0 0 20px #333; 86 | } 87 | .inverse h1, .inverse h2, .inverse h3 { 88 | color: #f3f3f3; 89 | } 90 | .title-slide .remark-slide-number { 91 | display: none; 92 | } 93 | /* Two-column layout */ 94 | .left-column { 95 | width: 20%; 96 | height: 92%; 97 | float: left; 98 | } 99 | .left-column h2, .left-column h3 { 100 | color: #00A8E199; 101 | } 102 | .left-column h2:last-of-type, .left-column h3:last-child { 103 | color: #00A8E1; 104 | } 105 | .right-column { 106 | width: 75%; 107 | float: right; 108 | padding-top: 1em; 109 | } 110 | .pull-left { 111 | float: left; 112 | width: 47%; 113 | } 114 | .pull-right { 115 | float: right; 116 | width: 47%; 117 | } 118 | .pull-right ~ * { 119 | clear: both; 120 | } 121 | img, video, iframe { 122 | max-width: 100%; 123 | } 124 | blockquote { 125 | border-left: solid 5px #FFEFBD80; 126 | padding-left: 1em; 127 | } 128 | .remark-slide table { 129 | margin: auto; 130 | border-top: 1px solid #666; 131 | border-bottom: 1px solid #666; 132 | } 133 | .remark-slide table thead th { border-bottom: 1px solid #ddd; } 134 | th, td { padding: 5px; } 135 | .remark-slide thead, .remark-slide tfoot, .remark-slide tr:nth-child(even) { background: #EEEEEE } 136 | table.dataTable tbody { 137 | background-color: #FFFFFF; 138 | color: #000000; 139 | } 140 | table.dataTable.display tbody tr.odd { 141 | background-color: #FFFFFF; 142 | } 143 | table.dataTable.display tbody tr.even { 144 | background-color: #EEEEEE; 145 | } 146 | table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { 147 | background-color: rgba(255, 255, 255, 0.5); 148 | } 149 | .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate { 150 | color: #000000; 151 | } 152 | .dataTables_wrapper .dataTables_paginate .paginate_button { 153 | color: #000000 !important; 154 | } 155 | 156 | @page { margin: 0; } 157 | @media print { 158 | .remark-slide-scaler { 159 | width: 100% !important; 160 | height: 100% !important; 161 | transform: scale(1) !important; 162 | top: 0 !important; 163 | left: 0 !important; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visualizing logistic regression results for non-technical audiences 2 | ### Abby Kaplan and Keiko Cawley 3 | 4 | Communicating the results of a logistic regression to a non-technical audience can be challenging because the parameters are on a log-odds scale. This repository contains several visualization options for presenting logistic regression results that are both interpretable and meaningful to your stakeholders who might not know what a log-odds or odds ratio is. 5 | 6 | 7 | ### Project links 8 | * A [vignette](https://keikcaw.github.io/visualizing-logistic-regression/Intro.html) which describes the pros and cons of each visualization, and in which situations one might choose one visualization over the others. This vignette also contains the code for each of the visuals. 9 | 10 | * A copy of the [slides](https://keikcaw.github.io/visualizing-logistic-regression/RUG_presentation.html) we used during our presentation to the SLC R Users Group on September 20, 2022. 11 | 12 | * The [link](https://www.youtube.com/watch?v=svHT7H1ZykA) to watch our Salt Lake R Users Group presentation on YouTube. 13 | 14 | * A copy of the [slides](https://keikcaw.github.io/visualizing-logistic-regression/AIR_presentation.html#1) we used during our presentation at AIR Forum on June 2, 2023. 15 | 16 | * A copy of the [slides](https://keikcaw.github.io/visualizing-logistic-regression/RMAIR_presentation.html#1) we used during our presentation at RMAIR on October 17, 2023. 17 | -------------------------------------------------------------------------------- /RMAIR_presentation/RMAIR_presentation.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Visualizing logistic regression results for non-technical audiences" 3 | author: "Abby Kaplan and Keiko Cawley" 4 | date: "October 17, 2023" 5 | output: 6 | xaringan::moon_reader: 7 | lib_dir: libs 8 | css: ["xaringan-themer-rmair.css"] 9 | nature: 10 | highlightStyle: github 11 | highlightLines: true 12 | countIncrementalSlides: false 13 | 14 | --- 15 | 16 | class: inverse, center, middle 17 | 18 | # GitHub 19 | ### https://github.com/keikcaw/visualizing-logistic-regression 20 | 21 | `r knitr::opts_knit$set(root.dir='..')` 22 | 23 | ```{r setup, include=FALSE} 24 | options(htmltools.dir.version = FALSE) 25 | library(dplyr) 26 | library(ggplot2) 27 | library(tidyverse) 28 | library(lme4) 29 | library(logitnorm) 30 | library(kableExtra) 31 | library(cowplot) 32 | library(grid) 33 | library(gridExtra) 34 | library(patchwork) 35 | library(knitr) 36 | theme_set(theme_bw()) 37 | opts_chunk$set(echo = F, message = F, warning = F, error = F, fig.retina = 3, 38 | fig.align = "center", fig.width = 6, fig.asp = 0.618, 39 | out.width = "70%") 40 | ``` 41 | 42 | ```{css} 43 | .remark-slide-content h1 { 44 | margin-bottom: 0em; 45 | } 46 | .remark-code { 47 | font-size: 60% !important; 48 | } 49 | ``` 50 | 51 | --- 52 | 53 | class: inverse, center, middle 54 | 55 | # Logistic regression review 56 | 57 | --- 58 | # Logistic regression: Binary outcomes 59 | 60 | - Use logistic regression to model a binary outcome 61 | 62 | -- 63 | 64 | - Examples from higher education: 65 | 66 | -- 67 | 68 | - Did the student pass the class? 69 | 70 | -- 71 | 72 | - Did the student enroll for another term? 73 | 74 | -- 75 | 76 | - Did the student graduate? 77 | 78 | --- 79 | 80 | # The design of logistic regression 81 | 82 | - We want to model the probability that the outcome happened 83 | 84 | -- 85 | 86 | - But probabilities are bounded between 0 and 1 87 | 88 | -- 89 | 90 | - Instead, we model the logit of the probability: 91 | 92 | $$ 93 | \mbox{logit}(p) = \log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 94 | $$ 95 | 96 | --- 97 | 98 | class: inverse, center, middle 99 | 100 | # What's the problem? 101 | 102 | --- 103 | 104 | layout: true 105 | 106 | # Just tell me "the" effect 107 | 108 | - Stakeholders often want to know whether something affects outcomes, and by how much 109 | 110 | --- 111 | 112 | -- 113 | 114 | - But we don't model probabilities directly 115 | 116 | $$ 117 | \log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 118 | $$ 119 | 120 | --- 121 | 122 | - But we don't model probabilities directly 123 | 124 | $$ 125 | \boxed{\log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right)} = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 126 | $$ 127 | 128 | -- 129 | 130 | - We can solve for _p_: 131 | 132 | $$ 133 | \begin{aligned} 134 | p & = \mbox{logit}^{-1}(\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n) \\ & \\ 135 | & = \frac{e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}}{1 + e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}} 136 | \end{aligned} 137 | $$ 138 | 139 | --- 140 | 141 | layout: true 142 | 143 | # "The" effect is nonlinear in _p_ 144 | 145 | $$ 146 | \begin{aligned} 147 | p & = \frac{e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}}{1 + e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}} 148 | \end{aligned} 149 | $$ 150 | 151 | --- 152 | 153 | -- 154 | 155 | ```{r} 156 | logistic.curve.0.p = ggplot() + 157 | stat_function(fun = function(x) (exp(x)/(1+(exp(x)))), color = "#009DD8") + 158 | scale_x_continuous(expression(beta[0]+beta[1]~x[1]~'+'~'...+'~beta[n]~x[n]), 159 | limits = c(-6, 6), breaks = seq(-6, 6, 2)) + 160 | scale_y_continuous("probability (p)", limits = c(0, 1)) 161 | logistic.curve.0.p 162 | ``` 163 | 164 | --- 165 | 166 | ```{r} 167 | logistic.curve.1.p = logistic.curve.0.p + 168 | annotate("segment", x = -3, xend = -2, y = invlogit(-3), yend = invlogit(-3), 169 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 170 | annotate("segment", x = -2, xend = -2, y = invlogit(-3), yend = invlogit(-2), 171 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 172 | annotate("text", x = -3.5, y = 0.3, 173 | label = str_wrap("A 1-unit change here leads to a small change in probability", 174 | 20)) 175 | logistic.curve.1.p 176 | ``` 177 | 178 | --- 179 | 180 | ```{r} 181 | logistic.curve.2.p = logistic.curve.1.p + 182 | annotate("segment", x = 0, xend = 1, y = invlogit(0), yend = invlogit(0), 183 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 184 | annotate("segment", x = 1, xend = 1, y = invlogit(0), yend = invlogit(1), 185 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 186 | annotate("text", x = 3, y = 0.6, 187 | label = str_wrap("A 1-unit change here leads to a large change in probability", 188 | 20)) 189 | logistic.curve.2.p 190 | ``` 191 | 192 | --- 193 | 194 | layout: false 195 | 196 | class: inverse, center, middle 197 | 198 | # Sample dataset and model 199 | 200 | --- 201 | 202 | # Dataset 203 | 204 | ```{r fit_model, include = F} 205 | knitr::read_chunk("R_code/fit_model.R") 206 | ``` 207 | 208 | - Our simulated dataset describes students who took Balloon Animal-Making 201 at University Imaginary 209 | 210 | -- 211 | 212 | ```{r dataset_summary_table} 213 | table <- data.frame( 214 | Variable = c("Mac user", "Wear glasses", "Pet type", "Favorite color", "Prior undergraduate GPA", "Height", "Went to tutoring", "Passed"), 215 | Possible.Responses = c("TRUE/FALSE", "TRUE/FALSE", "dog, cat, fish, none", "blue, red, green, orange", "0.0-4.0", "54-77 inches", "TRUE/FALSE", "TRUE/FALSE"), 216 | Variable.Type = c("binary", "binary", "categorical", "categorical", "continuous", "continuous", "binary", "binary") 217 | ) 218 | table %>% 219 | kbl(col.names = gsub("[.]", " ", names(table))) %>% 220 | kable_styling(bootstrap_options = c("striped", "hover"), full_width = F) 221 | ``` 222 | 223 | --- 224 | 225 | # Dataset 226 | 227 | ```{r load-data, echo = T} 228 | ``` 229 | 230 | ```{r preview_dataset} 231 | df %>% 232 | head(12) %>% 233 | kbl() %>% 234 | kable_styling(font_size = 12) 235 | ``` 236 | 237 | --- 238 | 239 | # Model 240 | 241 | - Dependent variable: did the student pass? 242 | 243 | -- 244 | 245 | - Continuous variables were centered and standardized 246 | 247 | ```{r prepare-data-continuous} 248 | ``` 249 | 250 | -- 251 | 252 | - Reference levels for categorical variables: 253 | 254 | - Pet type: none 255 | 256 | - Favorite color: blue 257 | 258 | ```{r prepare-data-categorical} 259 | ``` 260 | 261 | --- 262 | 263 | # Model 264 | 265 | ```{r model} 266 | ``` 267 | 268 | --- 269 | 270 | # Model 271 | 272 | ```{r model, highlight.output = c(4, 5, 7, 9, 11, 12, 13)} 273 | ``` 274 | 275 | 276 | --- 277 | 278 | # Causality disclaimer 279 | 280 | - Some visualizations strongly imply a causal interpretation 281 | 282 | -- 283 | 284 | - It's your responsibility to evaluate whether a causal interpretation is appropriate 285 | 286 | -- 287 | 288 | - If the data doesn't support a causal interpretation, **don't use a visualization that implies one** 289 | 290 | 291 | --- 292 | 293 | --- 294 | 295 | class: inverse, center, middle 296 | 297 | # Visualization family 1: 298 | 299 | # Presenting model coefficients 300 | 301 | 302 | --- 303 | 304 | # Coefficients in a table 305 | 306 | ``` {r get-coefficients, include = F} 307 | ``` 308 | 309 | ```{r} 310 | coefs.df %>% 311 | dplyr::select(-parameter) %>% 312 | kbl(col.names = c("Parameter", "Estimate", "Standard error", "z", "p")) %>% 313 | kable_styling(bootstrap_options = c("striped", "hover"), 314 | font_size = 14, full_width = F) %>% 315 | row_spec(0, align = "c") 316 | ``` 317 | 318 | --- 319 | 320 | # Coefficients in a table 321 | 322 | ![blinking_meme](blinking_meme.jpg) 323 | 324 | --- 325 | 326 | # Change in log odds 327 | 328 | ```{r colors, include = F} 329 | knitr::read_chunk("visual_folder/colors.R") 330 | ``` 331 | 332 | ```{r color-palette, include = F} 333 | ``` 334 | 335 | ```{r log_odds, include = F} 336 | knitr::read_chunk("visual_folder/log_odds.R") 337 | ``` 338 | 339 | ```{r change-in-log-odds, fig.show = "hide"} 340 | ``` 341 | 342 | ```{r change_in_log_odds_plot, out.width = "100%"} 343 | log.odds.p = log.odds.p + 344 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.1), clip = "off") 345 | log.odds.p.full = log.odds.p + 346 | ggtitle(str_wrap(gsub("\\n", " ", log.odds.p$labels$title), 50)) 347 | log.odds.p.full 348 | ``` 349 | 350 | --- 351 | 352 | # Change in log odds: Pros 353 | 354 | .pull-left[ 355 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 356 | log.odds.p.half = log.odds.p + 357 | guides(color = guide_legend(nrow = 3)) + 358 | theme(legend.position = "bottom") 359 | log.odds.p.half 360 | ``` 361 | ] 362 | 363 | -- 364 | 365 | .pull-right[ 366 | - It's clear which relationships are positive and which are negative 367 | 368 | {{content}} 369 | ] 370 | 371 | -- 372 | 373 | - The plot has a transparent relationship to the fitted model 374 | 375 | --- 376 | 377 | # Change in log odds: Pros 378 | 379 | .pull-left[ 380 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 381 | log.odds.p.half + 382 | annotate("rect", ymin = -1.2, ymax = 1.2, xmin = -0.5, xmax = 0.5, 383 | fill = NA, color = "red", size = 2) 384 | ``` 385 | ] 386 | 387 | .pull-right[ 388 | - It's clear which relationships are positive and which are negative 389 | 390 | - The plot has a transparent relationship to the fitted model 391 | 392 | - Numbers all in one place: a single scale instead of a table of numbers 393 | ] 394 | 395 | --- 396 | 397 | # Change in log odds: Cons 398 | 399 | .pull-left[ 400 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 401 | log.odds.p.half 402 | ``` 403 | ] 404 | 405 | --- 406 | 407 | # Change in log odds: Cons 408 | 409 | .pull-left[ 410 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 411 | log.odds.p.half + 412 | annotate("rect", ymin = -1, ymax = 0.57, xmin = -1.2, xmax = -0.2, 413 | fill = NA, color = "red", size = 2) 414 | ``` 415 | ] 416 | 417 | .pull-right[ 418 | - The magnitude of effect is in the log odds scale 419 | 420 | {{content}} 421 | ] 422 | 423 | -- 424 | 425 | - What is a 0.4 change in the log odds? 426 | 427 | {{content}} 428 | 429 | -- 430 | 431 | - Is the change between 0.4 and 0.8 log odds "big" or "small"? 432 | 433 | {{content}} 434 | 435 | -- 436 | 437 | - You probably don't want to give your audience a tutorial on the inverse logit function 438 | 439 | --- 440 | 441 | # Secret log odds 442 | 443 | ```{r change-in-log-odds-adjusted-axis, fig.show = "hide"} 444 | ``` 445 | 446 | ```{r change_in_log_odds_adjusted_axis_plot, out.width = "100%"} 447 | secret.log.odds.p = secret.log.odds.p + 448 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.5), clip = "off") 449 | secret.log.odds.p.full = secret.log.odds.p + 450 | ggtitle(str_wrap(gsub("\\n", " ", secret.log.odds.p$labels$title), 50)) 451 | secret.log.odds.p.full 452 | ``` 453 | 454 | --- 455 | 456 | # Secret log odds: Pros 457 | 458 | .pull-left[ 459 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 460 | secret.log.odds.p.half = secret.log.odds.p + 461 | guides(color = guide_legend(nrow = 3)) + 462 | theme(legend.position = "bottom") 463 | secret.log.odds.p.half 464 | ``` 465 | ] 466 | 467 | --- 468 | 469 | # Secret log odds: Pros 470 | 471 | .pull-left[ 472 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 473 | secret.log.odds.p.half + 474 | annotate("rect", ymin = -1.45, ymax = 1.45, xmin = -1.2, xmax = 0.5, 475 | fill = NA, color = "red", size = 2) 476 | ``` 477 | ] 478 | 479 | -- 480 | 481 | .pull-right[ 482 | - Easy: just relabel the x-axis 483 | 484 | {{content}} 485 | ] 486 | 487 | -- 488 | 489 | - No numbers for your audience to misinterpret 490 | 491 | --- 492 | 493 | # Secret log odds: Cons 494 | 495 | .pull-left[ 496 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 497 | secret.log.odds.p.half 498 | ``` 499 | ] 500 | 501 | -- 502 | 503 | .pull-right[ 504 | - Can't convey absolute magnitude of an effect 505 | 506 | {{content}} 507 | ] 508 | 509 | -- 510 | 511 | - Your audience might ask "where are the numbers?" anyway 512 | 513 | --- 514 | 515 | layout: true 516 | 517 | # Change in odds ratio 518 | 519 | - Your audience may be more familiar with the "odds" part of log odds 520 | 521 | --- 522 | 523 | --- 524 | 525 | $$\log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n$$ 526 | 527 | --- 528 | 529 | $$\log\left(\boxed{\begin{array}{c}\frac{p}{1 - p}\end{array}}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n$$ 530 | 531 | -- 532 | 533 | - Can't we just exponentiate to get the odds? 534 | 535 | -- 536 | 537 | $$\frac{p}{1 - p} = e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}$$ 538 | 539 | -- 540 | 541 | - Now the effect of a coefficient is multiplicative, not additive 542 | 543 | -- 544 | 545 | $$\frac{p}{1 - p} = e^{\beta_ix_i} \cdot e^{\beta_0 + \beta_1x_1 + \ldots + \beta_{i-1}x_{i-1} + \beta_{i+1}x_{i+1} + \ldots + \beta_nx_n}$$ 546 | 547 | --- 548 | 549 | layout: false 550 | 551 | # Change in odds ratio 552 | 553 | ```{r odds_ratio, include = F, cache = F} 554 | knitr::read_chunk("visual_folder/odds.R") 555 | ``` 556 | 557 | ```{r odds-ratio-adjusted-axis, fig.show = "hide"} 558 | ``` 559 | 560 | ```{r out.width = "100%"} 561 | odds.ratio.p = odds.ratio.p + 562 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 3), clip = "off") 563 | odds.ratio.p.full = odds.ratio.p + 564 | ggtitle(str_wrap(gsub("\\n", " ", odds.ratio.p$labels$title), 50)) 565 | odds.ratio.p.full 566 | ``` 567 | 568 | --- 569 | 570 | # Change in odds ratio: Pros 571 | 572 | .pull-left[ 573 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 574 | odds.ratio.p.half = odds.ratio.p + 575 | guides(color = guide_legend(nrow = 3)) + 576 | theme(legend.position = "bottom") 577 | odds.ratio.p.half 578 | ``` 579 | ] 580 | 581 | -- 582 | 583 | .pull-right[ 584 | - Changes in odds might be easier to describe than changes in log odds 585 | 586 | {{content}} 587 | ] 588 | 589 | -- 590 | 591 | - Still pretty easy: a simple transformation of your model coefficients 592 | 593 | --- 594 | 595 | # Change in odds ratio: Cons 596 | 597 | .pull-left[ 598 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 599 | odds.ratio.p.half 600 | ``` 601 | ] 602 | 603 | -- 604 | 605 | .pull-right[ 606 | - Not the way we usually describe odds 607 | ] 608 | 609 | --- 610 | 611 | # Change in odds ratio: Cons 612 | 613 | .pull-left[ 614 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 615 | odds.ratio.p.half 616 | ``` 617 | ] 618 | 619 | .pull-right[ 620 | - Not the way we usually describe odds 621 | 622 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 623 | ] 624 | 625 | --- 626 | 627 | # Change in odds ratio: Cons 628 | 629 | .pull-left[ 630 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 631 | odds.ratio.p.half 632 | ``` 633 | ] 634 | 635 | .pull-right[ 636 | - Not the way we usually describe odds 637 | 638 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 639 | 640 | - The unfamiliar format may undo the benefit of using a familiar concept 641 | 642 | {{content}} 643 | ] 644 | 645 | -- 646 | 647 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 648 | 649 | --- 650 | 651 | # Change in odds ratio: Cons 652 | 653 | .pull-left[ 654 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 655 | odds.ratio.p.half + 656 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = -1.2, xmax = 0.5, 657 | fill = NA, color = "red", size = 2) 658 | ``` 659 | ] 660 | 661 | .pull-right[ 662 | - Not the way we usually describe odds 663 | 664 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 665 | 666 | - The unfamiliar format may undo the benefit of using a familiar concept 667 | 668 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 669 | 670 | - Percent change in odds (300% = triple the odds) might be misinterpreted as a probability 671 | ] 672 | 673 | --- 674 | 675 | # Change in odds ratio: Cons 676 | 677 | .pull-left[ 678 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 679 | odds.ratio.p.half + 680 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = -1.2, xmax = 0.5, 681 | fill = NA, color = "red", size = 2) 682 | ``` 683 | ] 684 | 685 | .pull-right[ 686 | - Not the way we usually describe odds 687 | 688 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 689 | 690 | - The unfamiliar format may undo the benefit of using a familiar concept 691 | 692 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 693 | 694 | - Percent change in odds (300% = triple the odds) might be misinterpreted as a probability 695 | 696 | - Now we're pretty far removed from familiar scales 697 | ] 698 | 699 | --- 700 | 701 | # Change in odds ratio: Cons 702 | 703 | .pull-left[ 704 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 705 | log.odds.p.half 706 | ``` 707 | ] 708 | 709 | .pull-right[ 710 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 711 | odds.ratio.p.half 712 | ``` 713 | ] 714 | 715 | --- 716 | 717 | # Change in odds ratio: Cons 718 | 719 | .pull-left[ 720 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 721 | log.odds.p.half + 722 | annotate("rect", ymin = -1.8, ymax = 1.3, xmin = 10.5, xmax = 11.5, 723 | fill = NA, color = "red", size = 2) + 724 | annotate("rect", ymin = -1.8, ymax = 1.3, xmin = 0.5, xmax = 1.5, 725 | fill = NA, color = "red", size = 2) 726 | ``` 727 | ] 728 | 729 | .pull-right[ 730 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 731 | odds.ratio.p.half + 732 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = 10.5, xmax = 11.5, 733 | fill = NA, color = "red", size = 2) + 734 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = 0.5, xmax = 1.5, 735 | fill = NA, color = "red", size = 2) 736 | ``` 737 | ] 738 | 739 | - The scale is expanded for positive effects and compressed for negative effects 740 | 741 | --- 742 | 743 | class: inverse, center, middle 744 | 745 | # Visualization family 2: 746 | 747 | # Presenting probabilities 748 | 749 | --- 750 | 751 | # Probabilities relative to a baseline 752 | 753 | - Problem with probabilities: change in percentage points depends on baseline starting value 754 | 755 | -- 756 | 757 | - We can choose an appropriate baseline probability, then compute the marginal effect of a predictor given that baseline 758 | 759 | -- 760 | 761 | - Options for baseline: 762 | 763 | -- 764 | 765 | - Model intercept 766 | 767 | -- 768 | 769 | - Observed outcome % in dataset (similar to intercept if continuous predictors are centered and other coefficients aren't too large) 770 | 771 | -- 772 | 773 | - Observed outcome % for a certain group (e.g., students with no tutoring) 774 | 775 | -- 776 | 777 | - Some % that's meaningful in context (e.g., 85% pass rate in typical years) 778 | 779 | --- 780 | 781 | # Probabilities relative to a baseline 782 | 783 | - Baseline probability: inverse logit of the intercept 784 | 785 | $$p_0 = \mbox{logit}^{-1}(\beta_0)$$ 786 | 787 | -- 788 | 789 | - Probability with discrete predictor $i$: inverse logit of intercept + predictor coefficient 790 | 791 | $$p_i = \mbox{logit}^{-1}(\beta_0 + \beta_i)$$ 792 | 793 | -- 794 | 795 | - For a continuous predictor $j$, pick a change in predictor value that makes sense 796 | 797 | -- 798 | 799 | - One standard deviation 800 | 801 | -- 802 | 803 | - A context-specific benchmark (e.g., 1 point for GPA, 100 points on the SAT) 804 | 805 | -- 806 | 807 | $$p_j = \mbox{logit}^{-1}(\beta_0 + \beta_j\Delta x_j)$$ 808 | 809 | -- 810 | 811 | - To show uncertainty, get confidence interval before inverse logit transformation 812 | 813 | --- 814 | 815 | # Probabilities relative to a baseline 816 | 817 | ```{r probability_baseline, include = F, cache = F} 818 | knitr::read_chunk("visual_folder/probability_baseline.R") 819 | ``` 820 | 821 | ```{r probability-relative-to-some-baseline-no-arrows, fig.show = "hide"} 822 | ``` 823 | 824 | ```{r probability_baseline_plot, out.width = "100%"} 825 | prob.baseline.p = prob.baseline.p + 826 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 1), clip = "off") 827 | prob.baseline.p.full = prob.baseline.p + 828 | ggtitle(str_wrap(gsub("\\n", " ", prob.baseline.p$labels$title), 50)) 829 | prob.baseline.p.full 830 | ``` 831 | 832 | - (Uncertainty in intercept is not represented here) 833 | 834 | --- 835 | 836 | # Probabilities relative to a baseline: Pros 837 | 838 | .pull-left[ 839 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 840 | prob.baseline.p.half = prob.baseline.p + 841 | guides(color = guide_legend(nrow = 3)) + 842 | theme(legend.position = "bottom") 843 | prob.baseline.p.half 844 | ``` 845 | ] 846 | 847 | -- 848 | 849 | .pull-right[ 850 | - Familiar scale: probabilities, expressed as percentages 851 | 852 | {{content}} 853 | ] 854 | 855 | -- 856 | 857 | - Avoids the "percent change" formulation (common but misleading) 858 | 859 | --- 860 | 861 | # Probabilities relative to a baseline: Cons 862 | 863 | .pull-left[ 864 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 865 | prob.baseline.p.half + 866 | annotate("rect", ymin = invlogit(intercept) - 0.05, 867 | ymax = invlogit(intercept) + 0.05, xmin = 0, xmax = 12, 868 | fill = NA, color = "red", size = 2) 869 | ``` 870 | ] 871 | 872 | -- 873 | 874 | .pull-right[ 875 | - Have to choose a baseline; there may be no "good" choice 876 | 877 | {{content}} 878 | ] 879 | 880 | -- 881 | 882 | - Using the intercept as a baseline chooses reference categories for categorical variables 883 | 884 | --- 885 | 886 | # Probabilities relative to a baseline: Cons 887 | 888 | .pull-left[ 889 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 890 | prob.baseline.p.half + 891 | annotate("rect", ymin = invlogit(intercept) - 0.05, 892 | ymax = invlogit(intercept) + 0.05, xmin = 0, xmax = 12, 893 | fill = NA, color = "red", size = 2) 894 | ``` 895 | ] 896 | 897 | .pull-right[ 898 | - Have to choose a baseline; there may be no "good" choice 899 | 900 | - Using the intercept as a baseline chooses reference categories for categorical variables 901 | 902 | - Students who don't use Macs, don't wear glasses, etc. 903 | ] 904 | 905 | --- 906 | 907 | # Probabilities relative to a baseline: Cons 908 | 909 | .pull-left[ 910 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 911 | prob.baseline.p.half + 912 | annotate("rect", ymin = invlogit(intercept) - 0.05, 913 | ymax = invlogit(intercept) + 0.05, xmin = 0, xmax = 12, 914 | fill = NA, color = "red", size = 2) 915 | ``` 916 | ] 917 | 918 | .pull-right[ 919 | - Have to choose a baseline; there may be no "good" choice 920 | 921 | - Using the intercept as a baseline chooses reference categories for categorical variables 922 | 923 | - Students who don't use Macs, don't wear glasses, etc. 924 | 925 | - Not an appropriate choice for all datasets 926 | 927 | {{content}} 928 | ] 929 | 930 | -- 931 | 932 | - Doesn't show full range of possible effects at different baselines 933 | 934 | --- 935 | 936 | # Probabilities relative to a baseline: Arrows 937 | 938 | ```{r probability-relative-to-some-baseline-with-arrows, fig.show = "hide"} 939 | ``` 940 | 941 | ```{r probability_baseline_arrows_plot, out.width = "100%"} 942 | prob.baseline.arrows.p = prob.baseline.arrows.p + 943 | coord_cartesian(xlim = c(0, 1), ylim = c(0.9, 11.1), clip = "off") 944 | prob.baseline.arrows.p.full = prob.baseline.arrows.p + 945 | ggtitle(str_wrap(gsub("\\n", " ", prob.baseline.p$labels$title), 50)) 946 | prob.baseline.arrows.p.full 947 | ``` 948 | 949 | --- 950 | 951 | # Probabilities relative to a baseline: Arrows 952 | 953 | .pull-left[ 954 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 955 | prob.baseline.arrows.p.half = prob.baseline.arrows.p + 956 | guides(color = guide_legend(nrow = 3)) + 957 | theme(legend.position = "bottom") 958 | prob.baseline.arrows.p.half 959 | ``` 960 | ] 961 | 962 | -- 963 | 964 | .pull-right[ 965 | - Emphasizes direction of effect 966 | 967 | {{content}} 968 | ] 969 | 970 | -- 971 | 972 | - Doesn't show uncertainty around estimates 973 | 974 | {{content}} 975 | 976 | -- 977 | 978 | - Strong causal implications 979 | 980 | --- 981 | 982 | # Multiple baselines by group 983 | 984 | - Instead of one baseline probability, why not several? 985 | 986 | -- 987 | 988 | - Example: show effect of $i$ for each level of categorical variable $j$ 989 | 990 | -- 991 | 992 | $$p_{j1} = \mbox{logit}^{-1}(\beta_0 + \beta_{j1})$$ 993 | 994 | -- 995 | 996 | $$p_{j1 + i} = \mbox{logit}^{-1}(\beta_0 + \beta_{j1} + \beta_i)$$ 997 | 998 | --- 999 | 1000 | # Multiple baselines by group 1001 | 1002 | ```{r probability_group, include = F} 1003 | knitr::read_chunk("visual_folder/probability_group.R") 1004 | ``` 1005 | 1006 | ```{r probability-relative-to-some-baseline-and-group-no-arrows, fig.show = "hide"} 1007 | ``` 1008 | 1009 | ```{r probability_group_plot, out.width = "100%"} 1010 | prob.group.p = prob.group.p + 1011 | coord_flip(xlim = c(0.9, 8.1), ylim = c(0, 1), clip = "off") 1012 | prob.group.p.full = prob.group.p + 1013 | ggtitle(str_wrap(gsub("\\n", " ", prob.group.p$labels$title), 50)) + 1014 | theme(axis.text.y = element_text(size = 6)) 1015 | prob.group.p.full 1016 | ``` 1017 | 1018 | --- 1019 | 1020 | # Multiple baselines by group: Pros 1021 | 1022 | .pull-left[ 1023 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1024 | prob.group.p.half = prob.group.p + 1025 | guides(color = guide_legend(nrow = 3)) + 1026 | theme(legend.position = "bottom") 1027 | prob.group.p.half 1028 | ``` 1029 | ] 1030 | 1031 | -- 1032 | 1033 | .pull-right[ 1034 | - Emphasizes that the baseline we show is a _choice_ 1035 | ] 1036 | 1037 | --- 1038 | 1039 | # Multiple baselines by group: Pros 1040 | 1041 | .pull-left[ 1042 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1043 | prob.group.p.half + 1044 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 1045 | fill = NA, color = "red", size = 1) 1046 | ``` 1047 | ] 1048 | 1049 | .pull-right[ 1050 | - Emphasizes that the baseline we show is a _choice_ 1051 | 1052 | - Honors differences among groups 1053 | ] 1054 | 1055 | --- 1056 | 1057 | # Multiple baselines by group: Pros 1058 | 1059 | .pull-left[ 1060 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1061 | prob.group.p.half + 1062 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 1063 | fill = NA, color = "red", size = 1) 1064 | ``` 1065 | ] 1066 | 1067 | .pull-right[ 1068 | - Emphasizes that the baseline we show is a _choice_ 1069 | 1070 | - Honors differences among groups 1071 | 1072 | - Effect of GPA is larger for fish owners than for dog owners 1073 | ] 1074 | 1075 | --- 1076 | 1077 | # Multiple baselines by group: Pros 1078 | 1079 | .pull-left[ 1080 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1081 | prob.group.p.half + 1082 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 1083 | fill = NA, color = "red", size = 1) 1084 | ``` 1085 | ] 1086 | 1087 | .pull-right[ 1088 | - Emphasizes that the baseline we show is a _choice_ 1089 | 1090 | - Honors differences among groups 1091 | 1092 | - Effect of GPA is larger for fish owners than for dog owners 1093 | 1094 | - Here, this is purely because of fish owners' lower baseline 1095 | ] 1096 | 1097 | --- 1098 | 1099 | # Multiple baselines by group: Pros 1100 | 1101 | .pull-left[ 1102 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1103 | prob.group.p.half + 1104 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 1105 | fill = NA, color = "red", size = 1) 1106 | ``` 1107 | ] 1108 | 1109 | .pull-right[ 1110 | - Emphasizes that the baseline we show is a _choice_ 1111 | 1112 | - Honors differences among groups 1113 | 1114 | - Effect of GPA is larger for fish owners than for dog owners 1115 | 1116 | - Here, this is purely because of fish owners' lower baseline 1117 | 1118 | - But we could also show the effects of an interaction term in the model 1119 | 1120 | {{content}} 1121 | ] 1122 | 1123 | -- 1124 | 1125 | - We can use arrows here as well 1126 | 1127 | --- 1128 | 1129 | # Multiple baselines by group: Cons 1130 | 1131 | .pull-left[ 1132 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1133 | prob.group.p.half 1134 | ``` 1135 | ] 1136 | 1137 | -- 1138 | 1139 | .pull-right[ 1140 | - Still have to choose baselines by group 1141 | 1142 | {{content}} 1143 | ] 1144 | 1145 | -- 1146 | 1147 | - May suggest essentializing interpretations of groups 1148 | 1149 | {{content}} 1150 | 1151 | -- 1152 | 1153 | - Cluttered 1154 | 1155 | --- 1156 | 1157 | # Banana graphs 1158 | 1159 | - We can overcome the baseline-choosing problem by iterating across every baseline 1160 | 1161 | -- 1162 | 1163 | - For example: 1164 | 1165 | -- 1166 | 1167 | - Start with every possible probability of passing Balloon Animal-Making 201, from 0% to 100% (at sufficiently small intervals) 1168 | 1169 | -- 1170 | 1171 | - For each probability, add the effect of having a pet fish 1172 | 1173 | -- 1174 | 1175 | $$p_f = \mbox{logit}^{-1}(\mbox{logit}(p_0) + \beta_f)$$ 1176 | 1177 | --- 1178 | # Banana graphs 1179 | 1180 | ```{r banana_graphs, include = F} 1181 | knitr::read_chunk("visual_folder/banana_graphs.R") 1182 | ``` 1183 | 1184 | ```{r banana-graph, fig.show = "hide"} 1185 | banana.p = banana.p + 1186 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") 1187 | ``` 1188 | 1189 | ```{r out.width = "100%"} 1190 | banana.p 1191 | ``` 1192 | 1193 | --- 1194 | 1195 | # Banana graphs 1196 | 1197 | .pull-left[ 1198 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1199 | banana.p 1200 | ``` 1201 | ] 1202 | 1203 | -- 1204 | 1205 | .pull-right[ 1206 | - **x-axis:** baseline probability 1207 | 1208 | {{content}} 1209 | ] 1210 | 1211 | -- 1212 | 1213 | - **y-axis:** probability with effect of having a pet fish 1214 | 1215 | {{content}} 1216 | 1217 | -- 1218 | 1219 | - Solid line provides a reference (no effect) 1220 | 1221 | {{content}} 1222 | 1223 | -- 1224 | 1225 | - Positive effects above the line; negative effects below the line; no effect on the line 1226 | 1227 | --- 1228 | # Banana graphs 1229 | 1230 | ```{r out.width = "100%"} 1231 | banana.p + 1232 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") + 1233 | annotate("segment", x = 0.4, xend = 0.4, y = 0.4, 1234 | yend = invlogit(logit(0.4) + 1235 | coefs.df$est[coefs.df$parameter == "pet.typefish"]), 1236 | color = "black", size = 0.5, 1237 | arrow = arrow(type = "closed", length = unit(0.02, units = "npc"))) + 1238 | annotate("text", x = 0.1, y = 0.7, size = 2.90, hjust = 0, 1239 | label = str_wrap(paste("Some student who does not own a pet fish has a 40% chance of passing (x-axis value).", 1240 | " However, if that same student did own a pet fish,", 1241 | " their predicted probability of passing would be ", 1242 | round(invlogit(logit(0.4) + 1243 | coefs.df$est[coefs.df$parameter == "pet.typefish"]) * 100), 1244 | "% (y-axis value).", 1245 | sep = ""), 1246 | 30)) 1247 | ``` 1248 | 1249 | --- 1250 | 1251 | # Banana graphs 1252 | 1253 | ```{r banana-graph-multiple, fig.show = "hide"} 1254 | ``` 1255 | .center[ 1256 | ```{r out.width = "100%", fig.width = 9} 1257 | banana.multiple.p = banana.multiple.p + 1258 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") + 1259 | scale_fill_identity(guide = "legend", labels = c("Negative", "Not significant")) + 1260 | theme(legend.position = "right") + 1261 | labs(title ="Estimated relationship to probability of passing", 1262 | subtitle = "By pet type", 1263 | fill = "Relationship to\nprobability\nof passing") 1264 | banana.multiple.p 1265 | ``` 1266 | ] 1267 | 1268 | --- 1269 | 1270 | # Banana graphs: Pros 1271 | 1272 | .pull-left[ 1273 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1274 | banana.multiple.p 1275 | ``` 1276 | ] 1277 | 1278 | -- 1279 | 1280 | .pull-right[ 1281 | - Do not have to pick and choose a baseline 1282 | 1283 | {{content}} 1284 | ] 1285 | 1286 | -- 1287 | 1288 | - Show the whole range of predicted probabilities 1289 | 1290 | --- 1291 | 1292 | # Banana graphs: Cons 1293 | 1294 | .pull-left[ 1295 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1296 | banana.multiple.p 1297 | ``` 1298 | ] 1299 | 1300 | -- 1301 | 1302 | .pull-right[ 1303 | - Can take up quite a bit of space 1304 | 1305 | {{content}} 1306 | ] 1307 | 1308 | -- 1309 | 1310 | - May be initially difficult to understand 1311 | 1312 | {{content}} 1313 | 1314 | -- 1315 | 1316 | - Predictor variables are in separate graphs: hard to compare 1317 | 1318 | --- 1319 | 1320 | class: inverse, center, middle 1321 | 1322 | # Visualization family 3: 1323 | 1324 | # Counterfactual counts 1325 | 1326 | --- 1327 | 1328 | # Extra successes 1329 | 1330 | - Sometimes stakeholders are interested in **the number of times something happens (or doesn't happen)** 1331 | 1332 | -- 1333 | 1334 | - Example: stakeholders want to assess the impact of tutoring on pass rates in Balloon Animal-Making 201 1335 | 1336 | -- 1337 | 1338 | - They're interested not just in _whether_ tutoring helps students, but _how much_ it helps them 1339 | 1340 | -- 1341 | 1342 | - In our dataset, `r format(sum(df$tutoring), big.mark = ",")` students received tutoring; of those, `r format(sum(df$tutoring & df$passed), big.mark = ",")` passed the class 1343 | 1344 | -- 1345 | 1346 | - Suppose those students had _not_ received tutoring; in that case, how many would have passed? 1347 | 1348 | -- 1349 | 1350 | - In other words, how many "extra" passes did we get because of tutoring? 1351 | 1352 | --- 1353 | 1354 | # Extra successes 1355 | 1356 | - To get a point estimate: 1357 | 1358 | -- 1359 | 1360 | - Take all students who received tutoring 1361 | 1362 | -- 1363 | 1364 | - Set `tutoring` to `FALSE` instead of `TRUE` 1365 | 1366 | -- 1367 | 1368 | - Use the model to make (counterfactual) predictions for the revised dataset 1369 | 1370 | -- 1371 | 1372 | - Count predicted counterfactual passes; compare to the actual number of passes 1373 | 1374 | -- 1375 | 1376 | - We can get confidence intervals by simulating many sets of outcomes and aggregating over them. 1377 | 1378 | --- 1379 | 1380 | # Extra successes 1381 | 1382 | ```{r counterfactuals, include = F} 1383 | knitr::read_chunk("visual_folder/counterfactuals.R") 1384 | ``` 1385 | 1386 | ```{r extra-passes, fig.show = "hide"} 1387 | ``` 1388 | 1389 | ```{r extra_passes_plot, out.width = "100%"} 1390 | extra.p 1391 | ``` 1392 | 1393 | --- 1394 | 1395 | # Extra successes: Pros 1396 | 1397 | .pull-left[ 1398 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1399 | extra.p 1400 | ``` 1401 | ] 1402 | 1403 | -- 1404 | 1405 | .pull-right[ 1406 | - Counts have a straightforward interpretation 1407 | 1408 | {{content}} 1409 | ] 1410 | 1411 | -- 1412 | 1413 | - Natural baseline: account for other characteristics of your population (e.g., number of fish owners) 1414 | 1415 | --- 1416 | 1417 | # Extra successes: Cons 1418 | 1419 | .pull-left[ 1420 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1421 | extra.p 1422 | ``` 1423 | ] 1424 | 1425 | -- 1426 | 1427 | .pull-right[ 1428 | - "Number of simulations" may be hard to explain 1429 | 1430 | {{content}} 1431 | ] 1432 | 1433 | -- 1434 | 1435 | - Assumes that the counterfactual makes sense 1436 | 1437 | {{content}} 1438 | 1439 | -- 1440 | 1441 | - Strong causal interpretation 1442 | 1443 | --- 1444 | 1445 | # Extra successes by group 1446 | 1447 | - Your stakeholders may be interested in different effects by group 1448 | 1449 | -- 1450 | 1451 | - We can summarize counterfactuals for separate groups 1452 | 1453 | --- 1454 | 1455 | # Extra successes by group 1456 | 1457 | ```{r extra-passes-by-group, fig.show = "hide"} 1458 | ``` 1459 | 1460 | ```{r out.width = "100%"} 1461 | extra.group.p 1462 | ``` 1463 | 1464 | --- 1465 | 1466 | # Extra successes by group: Pros 1467 | 1468 | .pull-left[ 1469 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1470 | extra.group.p 1471 | ``` 1472 | ] 1473 | 1474 | -- 1475 | 1476 | .pull-right[ 1477 | - Avoids a scale with number of simulations; focus is on range of predictions 1478 | 1479 | {{content}} 1480 | ] 1481 | 1482 | -- 1483 | 1484 | - Shows differences by group 1485 | 1486 | {{content}} 1487 | 1488 | -- 1489 | 1490 | - Interaction terms in the model would be incorporated automatically 1491 | 1492 | --- 1493 | 1494 | # Extra successes by group: Cons 1495 | 1496 | .pull-left[ 1497 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1498 | extra.group.p 1499 | ``` 1500 | ] 1501 | 1502 | -- 1503 | 1504 | .pull-right[ 1505 | - Doesn't show how absolute numbers depend on group size 1506 | 1507 | {{content}} 1508 | ] 1509 | 1510 | -- 1511 | 1512 | - Tutoring actually has a _larger_ percentage point effect for fish owners (because of the lower baseline), but the group is small 1513 | 1514 | {{content}} 1515 | 1516 | -- 1517 | 1518 | - (Your audience may care about counts, percentages, or both) 1519 | 1520 | --- 1521 | 1522 | # Potential successes compared to group size 1523 | 1524 | - Attempt to show _both_ the effect size for each group _and_ the overall size of that group 1525 | 1526 | -- 1527 | 1528 | - Here, we switch the direction of the counterfactual 1529 | 1530 | -- 1531 | 1532 | - Start with untutored students 1533 | 1534 | -- 1535 | 1536 | - How many would have passed with tutoring? 1537 | 1538 | -- 1539 | 1540 | - We think this emphasizes the benefits of tutoring more clearly in this graph 1541 | 1542 | -- 1543 | 1544 | - Either direction is possible; do what makes sense in your context! 1545 | 1546 | --- 1547 | 1548 | # Potential successes compared to group size 1549 | 1550 | ```{r potential-passes-by-group, fig.show = "hide"} 1551 | ``` 1552 | 1553 | ```{r out.width = "100%"} 1554 | potential.group.p 1555 | ``` 1556 | 1557 | --- 1558 | 1559 | # Potential successes compared to group size 1560 | 1561 | .pull-left[ 1562 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 1563 | potential.group.p.half = potential.group.p + 1564 | theme(legend.position = "bottom") 1565 | potential.group.p.half 1566 | ``` 1567 | ] 1568 | 1569 | -- 1570 | 1571 | .pull-right[ 1572 | - Acknowledges different group sizes: puts absolute numbers in context 1573 | 1574 | {{content}} 1575 | ] 1576 | 1577 | -- 1578 | 1579 | - But small groups are squished at the bottom of the scale (hard to see) 1580 | 1581 | --- 1582 | 1583 | # Conclusion 1584 | 1585 | - There is no right or wrong way, only better and worse ways for a particular project, so get creative! 1586 | 1587 | -- 1588 | 1589 | - Knowing your stakeholders as well as the context and purpose of your research should be your guides to determine which visualization is most appropriate 1590 | 1591 | -- 1592 | 1593 | - Use colors, the layout, and annotations to your advantage 1594 | 1595 | -- 1596 | 1597 | - Share your ideas with others 1598 | 1599 | --- 1600 | 1601 | class: inverse, center, middle 1602 | 1603 | # Thank you! 1604 | -------------------------------------------------------------------------------- /RMAIR_presentation/blinking_meme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikcaw/visualizing-logistic-regression/a97dafc9fab6cedce41b0a41ea88f1baece8474e/RMAIR_presentation/blinking_meme.jpg -------------------------------------------------------------------------------- /RMAIR_presentation/xaringan-themer-rmair.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------- 2 | * 3 | * !! This file was generated by xaringanthemer !! 4 | * 5 | * Changes made to this file directly will be overwritten 6 | * if you used xaringanthemer in your xaringan slides Rmd 7 | * 8 | * Issues or likes? 9 | * - https://github.com/gadenbuie/xaringanthemer 10 | * - https://www.garrickadenbuie.com 11 | * 12 | * Need help? Try: 13 | * - vignette(package = "xaringanthemer") 14 | * - ?xaringanthemer::write_xaringan_theme 15 | * - xaringan wiki: https://github.com/yihui/xaringan/wiki 16 | * - remarkjs wiki: https://github.com/gnab/remark/wiki 17 | * 18 | * ------------------------------------------------------- */ 19 | @import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic); 20 | @import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz); 21 | @import url(https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700); 22 | 23 | 24 | body { 25 | font-family: 'Droid Serif', 'Palatino Linotype', 'Book Antiqua', Palatino, 'Microsoft YaHei', 'Songti SC', serif; 26 | font-weight: normal; 27 | color: #000000; 28 | } 29 | h1, h2, h3 { 30 | font-family: 'Yanone Kaffeesatz'; 31 | font-weight: normal; 32 | background-color: #003865; 33 | color: #FFCD00; 34 | } 35 | .remark-slide-content { 36 | background-color: #FFFFFF; 37 | font-size: 20px; 38 | padding: 1em 4em 1em 4em; 39 | } 40 | .remark-slide-content h1 { 41 | font-size: 55px; 42 | padding: 10px 10px 0px 10px; 43 | } 44 | .remark-slide-content h2 { 45 | font-size: 45px; 46 | } 47 | .remark-slide-content h3 { 48 | font-size: 35px; 49 | } 50 | .remark-code, .remark-inline-code { 51 | font-family: 'Source Code Pro', 'Lucida Console', Monaco, monospace; 52 | } 53 | .remark-code { 54 | font-size: 0.9em; 55 | } 56 | .remark-inline-code { 57 | font-size: 1em; 58 | color: #00A8E1; 59 | 60 | 61 | } 62 | .remark-slide-number { 63 | color: #00A8E1; 64 | opacity: 1; 65 | font-size: 0.9em; 66 | } 67 | .inverse > .remark-slide-number { 68 | color: #FFCD00; 69 | opacity: 1; 70 | font-size: 0.9em; 71 | } 72 | strong{color:#00A8E1;} 73 | a, a > code { 74 | color: #FFCD00; 75 | text-decoration: none; 76 | } 77 | .footnote { 78 | 79 | position: absolute; 80 | bottom: 3em; 81 | padding-right: 4em; 82 | font-size: 0.9em; 83 | } 84 | .remark-code-line-highlighted { 85 | background-color: rgba(255,239,189,0.5); 86 | } 87 | .inverse { 88 | background-color: #003865; 89 | color: #FFCD00; 90 | text-shadow: 0 0 20px #333; 91 | } 92 | .inverse h1, .inverse h2, .inverse h3 { 93 | color: #FFCD00; 94 | } 95 | .title-slide .remark-slide-number { 96 | display: none; 97 | } 98 | /* Two-column layout */ 99 | .left-column { 100 | width: 20%; 101 | height: 92%; 102 | float: left; 103 | } 104 | .left-column h2, .left-column h3 { 105 | color: #00A8E199; 106 | } 107 | .left-column h2:last-of-type, .left-column h3:last-child { 108 | color: #00A8E1; 109 | } 110 | .right-column { 111 | width: 75%; 112 | float: right; 113 | padding-top: 1em; 114 | } 115 | .pull-left { 116 | float: left; 117 | width: 47%; 118 | } 119 | .pull-right { 120 | float: right; 121 | width: 47%; 122 | } 123 | .pull-right ~ * { 124 | clear: both; 125 | } 126 | img, video, iframe { 127 | max-width: 100%; 128 | } 129 | blockquote { 130 | border-left: solid 5px #FFEFBD80; 131 | padding-left: 1em; 132 | } 133 | .remark-slide table { 134 | margin: auto; 135 | border-top: 1px solid #666; 136 | border-bottom: 1px solid #666; 137 | } 138 | .remark-slide table thead th { border-bottom: 1px solid #ddd; } 139 | th, td { padding: 5px; } 140 | .remark-slide thead, .remark-slide tfoot, .remark-slide tr:nth-child(even) { background: #EEEEEE } 141 | table.dataTable tbody { 142 | background-color: #FFFFFF; 143 | color: #000000; 144 | } 145 | table.dataTable.display tbody tr.odd { 146 | background-color: #FFFFFF; 147 | } 148 | table.dataTable.display tbody tr.even { 149 | background-color: #EEEEEE; 150 | } 151 | table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { 152 | background-color: rgba(255, 255, 255, 0.5); 153 | } 154 | .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate { 155 | color: #000000; 156 | } 157 | .dataTables_wrapper .dataTables_paginate .paginate_button { 158 | color: #000000 !important; 159 | } 160 | 161 | @page { margin: 0; } 162 | @media print { 163 | .remark-slide-scaler { 164 | width: 100% !important; 165 | height: 100% !important; 166 | transform: scale(1) !important; 167 | top: 0 !important; 168 | left: 0 !important; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /RUG_presentation/RUG_presentation.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Visualizing logistic regression results for non-technical audiences" 3 | author: "Abby Kaplan and Keiko Cawley" 4 | date: "September 20, 2022" 5 | output: 6 | xaringan::moon_reader: 7 | lib_dir: libs 8 | css: ["xaringan-themer.css"] 9 | nature: 10 | highlightStyle: github 11 | highlightLines: true 12 | countIncrementalSlides: false 13 | 14 | --- 15 | 16 | class: inverse, center, middle 17 | 18 | # GitHub 19 | ### https://github.com/keikcaw/visualizing-logistic-regression 20 | 21 | `r knitr::opts_knit$set(root.dir='..')` 22 | 23 | ```{r setup, include=FALSE} 24 | options(htmltools.dir.version = FALSE) 25 | library(dplyr) 26 | library(ggplot2) 27 | library(tidyverse) 28 | library(lme4) 29 | library(logitnorm) 30 | library(kableExtra) 31 | library(cowplot) 32 | library(grid) 33 | library(gridExtra) 34 | library(patchwork) 35 | library(knitr) 36 | theme_set(theme_bw()) 37 | opts_chunk$set(echo = F, message = F, warning = F, error = F, fig.retina = 3, 38 | fig.align = "center", fig.width = 6, fig.asp = 0.618, 39 | out.width = "70%") 40 | ``` 41 | 42 | ```{css} 43 | .remark-slide-content h1 { 44 | margin-bottom: 0em; 45 | } 46 | .remark-code { 47 | font-size: 60% !important; 48 | } 49 | ``` 50 | 51 | --- 52 | 53 | class: inverse, center, middle 54 | 55 | # Logistic regression review 56 | 57 | --- 58 | # Logistic regression: Binary outcomes 59 | 60 | - Use logistic regression to model a binary outcome 61 | 62 | -- 63 | 64 | - Examples from higher education: 65 | 66 | -- 67 | 68 | - Did the student pass the class? 69 | 70 | -- 71 | 72 | - Did the student enroll for another term? 73 | 74 | -- 75 | 76 | - Did the student graduate? 77 | 78 | --- 79 | 80 | # The design of logistic regression 81 | 82 | - We want to model the probability that the outcome happened 83 | 84 | -- 85 | 86 | - But probabilities are bounded between 0 and 1 87 | 88 | -- 89 | 90 | - Instead, we model the logit of the probability: 91 | 92 | $$ 93 | \mbox{logit}(p) = \log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 94 | $$ 95 | 96 | --- 97 | 98 | class: inverse, center, middle 99 | 100 | # What's the problem? 101 | 102 | --- 103 | 104 | layout: true 105 | 106 | # Just tell me "the" effect 107 | 108 | - Stakeholders often want to know whether something affects outcomes, and by how much 109 | 110 | --- 111 | 112 | -- 113 | 114 | - But we don't model probabilities directly 115 | 116 | $$ 117 | \log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 118 | $$ 119 | 120 | --- 121 | 122 | - But we don't model probabilities directly 123 | 124 | $$ 125 | \boxed{\log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right)} = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n 126 | $$ 127 | 128 | -- 129 | 130 | - We can solve for _p_: 131 | 132 | $$ 133 | \begin{aligned} 134 | p & = \mbox{logit}^{-1}(\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n) \\ & \\ 135 | & = \frac{e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}}{1 + e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}} 136 | \end{aligned} 137 | $$ 138 | 139 | --- 140 | 141 | layout: true 142 | 143 | # "The" effect is nonlinear in _p_ 144 | 145 | $$ 146 | \begin{aligned} 147 | p & = \frac{e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}}{1 + e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}} 148 | \end{aligned} 149 | $$ 150 | 151 | --- 152 | 153 | -- 154 | 155 | ```{r} 156 | logistic.curve.0.p = ggplot() + 157 | stat_function(fun = function(x) (exp(x)/(1+(exp(x)))), color = "#009DD8") + 158 | scale_x_continuous(expression(beta[0]+beta[1]~x[1]~'+'~'...+'~beta[n]~x[n]), 159 | limits = c(-6, 6), breaks = seq(-6, 6, 2)) + 160 | scale_y_continuous("probability (p)", limits = c(0, 1)) 161 | logistic.curve.0.p 162 | ``` 163 | 164 | --- 165 | 166 | ```{r} 167 | logistic.curve.1.p = logistic.curve.0.p + 168 | annotate("segment", x = -3, xend = -2, y = invlogit(-3), yend = invlogit(-3), 169 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 170 | annotate("segment", x = -2, xend = -2, y = invlogit(-3), yend = invlogit(-2), 171 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 172 | annotate("text", x = -3.5, y = 0.3, 173 | label = str_wrap("A 1-unit change here leads to a small change in probability", 174 | 20)) 175 | logistic.curve.1.p 176 | ``` 177 | 178 | --- 179 | 180 | ```{r} 181 | logistic.curve.2.p = logistic.curve.1.p + 182 | annotate("segment", x = 0, xend = 1, y = invlogit(0), yend = invlogit(0), 183 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 184 | annotate("segment", x = 1, xend = 1, y = invlogit(0), yend = invlogit(1), 185 | arrow = arrow(type = "closed", length = unit(0.02, "npc"))) + 186 | annotate("text", x = 3, y = 0.6, 187 | label = str_wrap("A 1-unit change here leads to a large change in probability", 188 | 20)) 189 | logistic.curve.2.p 190 | ``` 191 | 192 | --- 193 | 194 | layout: false 195 | 196 | class: inverse, center, middle 197 | 198 | # Sample dataset and model 199 | 200 | --- 201 | 202 | # Dataset 203 | 204 | ```{r fit_model, include = F} 205 | knitr::read_chunk("R_code/fit_model.R") 206 | ``` 207 | 208 | - Our simulated dataset describes students who took Balloon Animal-Making 201 at University Imaginary 209 | 210 | -- 211 | 212 | ```{r dataset_summary_table} 213 | table <- data.frame( 214 | Variable = c("Mac user", "Wear glasses", "Pet type", "Favorite color", "Prior undergraduate GPA", "Height", "Went to tutoring", "Passed"), 215 | Possible.Responses = c("TRUE/FALSE", "TRUE/FALSE", "dog, cat, fish, none", "blue, red, green, orange", "0.0-4.0", "54-77 inches", "TRUE/FALSE", "TRUE/FALSE"), 216 | Variable.Type = c("binary", "binary", "categorical", "categorical", "continuous", "continuous", "binary", "binary") 217 | ) 218 | table %>% 219 | kbl(col.names = gsub("[.]", " ", names(table))) %>% 220 | kable_styling(bootstrap_options = c("striped", "hover"), full_width = F) 221 | ``` 222 | 223 | --- 224 | 225 | # Dataset 226 | 227 | ```{r load-data, echo = T} 228 | ``` 229 | 230 | ```{r preview_dataset} 231 | df %>% 232 | head(12) %>% 233 | kbl() %>% 234 | kable_styling(font_size = 12) 235 | ``` 236 | 237 | --- 238 | 239 | # Model 240 | 241 | - Dependent variable: did the student pass? 242 | 243 | -- 244 | 245 | - Continuous variables were centered and standardized 246 | 247 | ```{r prepare-data-continuous, echo = T} 248 | ``` 249 | 250 | -- 251 | 252 | - Reference levels for categorical variables: 253 | 254 | - Pet type: none 255 | 256 | - Favorite color: blue 257 | 258 | ```{r prepare-data-categorical, echo = T} 259 | ``` 260 | 261 | --- 262 | 263 | # Model 264 | 265 | ```{r model, echo = T} 266 | ``` 267 | 268 | --- 269 | 270 | # Model 271 | 272 | ```{r model, echo = T, highlight.output = c(4, 5, 7, 9, 11, 12, 13)} 273 | ``` 274 | 275 | 276 | --- 277 | 278 | # Causality disclaimer 279 | 280 | - Some visualizations strongly imply a causal interpretation 281 | 282 | -- 283 | 284 | - It's your responsibility to evaluate whether a causal interpretation is appropriate 285 | 286 | -- 287 | 288 | - If the data doesn't support a causal interpretation, **don't use a visualization that implies one** 289 | 290 | 291 | --- 292 | 293 | # Model coefficients 294 | 295 | ``` {r get-coefficients, echo = T} 296 | ``` 297 | 298 | --- 299 | 300 | # Color palette 301 | 302 | ```{r colors, include = F} 303 | knitr::read_chunk("visual_folder/colors.R") 304 | ``` 305 | 306 | ```{r color-palette, echo = T} 307 | ``` 308 | 309 | ```{r} 310 | data.frame(color.name = paste(c("good", "neutral", "bad"), "color", sep = "."), 311 | hex.code = c(good.color, neutral.color, bad.color)) %>% 312 | mutate(color.name = fct_relevel(color.name, "good.color", 313 | "neutral.color")) %>% 314 | ggplot(aes(x = color.name, y = 1, color = hex.code)) + 315 | geom_point(size = 30) + 316 | scale_color_identity() + 317 | theme_void() + 318 | theme(axis.text.x = element_text(size = 14, margin = margin(t = 0, b = 5))) 319 | ``` 320 | 321 | --- 322 | 323 | class: inverse, center, middle 324 | 325 | # Visualization family 1: 326 | 327 | # Presenting model coefficients 328 | 329 | 330 | --- 331 | 332 | # Coefficients in a table 333 | 334 | ```{r} 335 | coefs.df %>% 336 | dplyr::select(-parameter) %>% 337 | kbl(col.names = c("Parameter", "Estimate", "Standard error", "z", "p")) %>% 338 | kable_styling(bootstrap_options = c("striped", "hover"), 339 | font_size = 14, full_width = F) %>% 340 | row_spec(0, align = "c") 341 | ``` 342 | 343 | --- 344 | 345 | # Coefficients in a table 346 | 347 | ![blinking_meme](blinking_meme.jpg) 348 | 349 | --- 350 | 351 | # Change in log odds 352 | 353 | ```{r log_odds, include = F} 354 | knitr::read_chunk("visual_folder/log_odds.R") 355 | ``` 356 | 357 | .pull-left[ 358 | ```{r change-in-log-odds, echo = T, fig.show = "hide"} 359 | ``` 360 | ] 361 | 362 | .pull-right[ 363 | ```{r change_in_log_odds_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 364 | log.odds.p + 365 | guides(color = guide_legend(nrow = 3)) + 366 | theme(legend.position = "bottom") + 367 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.1), clip = "off") 368 | ``` 369 | ] 370 | 371 | --- 372 | 373 | # Change in log odds: Pros 374 | 375 | .pull-left[ 376 | ```{r ref.label = "change_in_log_odds_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 377 | ``` 378 | ] 379 | 380 | -- 381 | 382 | .pull-right[ 383 | - It's clear which relationships are positive and which are negative 384 | 385 | {{content}} 386 | ] 387 | 388 | -- 389 | 390 | - The plot has a transparent relationship to the fitted model 391 | 392 | --- 393 | 394 | # Change in log odds: Pros 395 | 396 | .pull-left[ 397 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 398 | log.odds.p + 399 | guides(color = guide_legend(nrow = 3)) + 400 | theme(legend.position = "bottom") + 401 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.1), clip = "off") + 402 | annotate("rect", ymin = -1.2, ymax = 1.2, xmin = -0.5, xmax = 0.5, 403 | fill = NA, color = "red", size = 2) 404 | ``` 405 | ] 406 | 407 | .pull-right[ 408 | - It's clear which relationships are positive and which are negative 409 | 410 | - The plot has a transparent relationship to the fitted model 411 | 412 | - Numbers all in one place: a single scale instead of a table of numbers 413 | ] 414 | 415 | --- 416 | 417 | # Change in log odds: Cons 418 | 419 | .pull-left[ 420 | ```{r ref.label = "change_in_log_odds_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 421 | ``` 422 | ] 423 | 424 | --- 425 | 426 | # Change in log odds: Cons 427 | 428 | .pull-left[ 429 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 430 | log.odds.p + 431 | guides(color = guide_legend(nrow = 3)) + 432 | theme(legend.position = "bottom") + 433 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.1), clip = "off") + 434 | annotate("rect", ymin = -1, ymax = 0.57, xmin = -1.2, xmax = -0.2, 435 | fill = NA, color = "red", size = 2) 436 | ``` 437 | ] 438 | 439 | .pull-right[ 440 | - The magnitude of effect is in the log odds scale 441 | 442 | {{content}} 443 | ] 444 | 445 | -- 446 | 447 | - What is a 0.4 change in the log odds? 448 | 449 | {{content}} 450 | 451 | -- 452 | 453 | - Is the change between 0.4 and 0.8 log odds "big" or "small"? 454 | 455 | {{content}} 456 | 457 | -- 458 | 459 | - You probably don't want to give your audience a tutorial on the inverse logit function 460 | 461 | --- 462 | 463 | # Secret log odds 464 | 465 | .pull-left[ 466 | ```{r change-in-log-odds-adjusted-axis, echo = T, fig.show = "hide"} 467 | ``` 468 | ] 469 | 470 | .pull-right[ 471 | ```{r change_in_log_odds_adjusted_axis_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 472 | secret.log.odds.p + 473 | guides(color = guide_legend(nrow = 3)) + 474 | theme(legend.position = "bottom") + 475 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.5), clip = "off") 476 | ``` 477 | ] 478 | 479 | --- 480 | 481 | # Secret log odds: Pros 482 | 483 | .pull-left[ 484 | ```{r ref.label = "change_in_log_odds_adjusted_axis_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 485 | ``` 486 | ] 487 | 488 | --- 489 | 490 | # Secret log odds: Pros 491 | 492 | .pull-left[ 493 | ```{r change_in_log_adds_adjusted_axis_highlighted_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 494 | secret.log.odds.p + 495 | guides(color = guide_legend(nrow = 3)) + 496 | theme(legend.position = "bottom") + 497 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.5), clip = "off") + 498 | annotate("rect", ymin = -1.45, ymax = 1.45, xmin = -1.2, xmax = 0.5, 499 | fill = NA, color = "red", size = 2) 500 | ``` 501 | ] 502 | 503 | -- 504 | 505 | .pull-right[ 506 | - Easy: just relabel the x-axis 507 | 508 | {{content}} 509 | ] 510 | 511 | -- 512 | 513 | - No numbers for your audience to misinterpret 514 | 515 | --- 516 | 517 | # Secret log odds: Cons 518 | 519 | .pull-left[ 520 | ```{r ref.label = "change_in_log_adds_adjusted_axis_highlighted_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 521 | ``` 522 | ] 523 | 524 | -- 525 | 526 | .pull-right[ 527 | - Can't convey absolute magnitude of an effect 528 | 529 | {{content}} 530 | ] 531 | 532 | -- 533 | 534 | - Your audience might ask "where are the numbers?" anyway 535 | 536 | --- 537 | 538 | layout: true 539 | 540 | # Change in odds ratio 541 | 542 | - Your audience may be more familiar with the "odds" part of log odds 543 | 544 | --- 545 | 546 | --- 547 | 548 | $$\log\left(\begin{array}{c}\frac{p}{1 - p}\end{array}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n$$ 549 | 550 | --- 551 | 552 | $$\log\left(\boxed{\begin{array}{c}\frac{p}{1 - p}\end{array}}\right) = \beta_0 + \beta_1x_1 + \ldots + \beta_nx_n$$ 553 | 554 | -- 555 | 556 | - Can't we just exponentiate to get the odds? 557 | 558 | -- 559 | 560 | $$\frac{p}{1 - p} = e^{\beta_0 + \beta_1x_1 + \ldots + \beta_nx_n}$$ 561 | 562 | -- 563 | 564 | - Now the effect of a coefficient is multiplicative, not additive 565 | 566 | -- 567 | 568 | $$\frac{p}{1 - p} = e^{\beta_ix_i} \cdot e^{\beta_0 + \beta_1x_1 + \ldots + \beta_{i-1}x_{i-1} + \beta_{i+1}x_{i+1} + \ldots + \beta_nx_n}$$ 569 | 570 | --- 571 | 572 | layout: false 573 | 574 | # Change in odds ratio 575 | 576 | ```{r odds_ratio, include = F, cache = F} 577 | knitr::read_chunk("visual_folder/odds.R") 578 | ``` 579 | 580 | .pull-left[ 581 | ```{r odds-ratio-adjusted-axis, echo = T, fig.show = "hide"} 582 | ``` 583 | ] 584 | 585 | .pull-right[ 586 | ```{r odds_ratio_adjusted_axis_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 587 | odds.ratio.p + 588 | guides(color = guide_legend(nrow = 3)) + 589 | theme(legend.position = "bottom") + 590 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 3), clip = "off") 591 | ``` 592 | ] 593 | 594 | --- 595 | 596 | # Change in odds ratio: Pros 597 | 598 | .pull-left[ 599 | ```{r ref.label = "odds_ratio_adjusted_axis_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 600 | ``` 601 | ] 602 | 603 | -- 604 | 605 | .pull-right[ 606 | - Changes in odds might be easier to describe than changes in log odds 607 | 608 | {{content}} 609 | ] 610 | 611 | -- 612 | 613 | - Still pretty easy: a simple transformation of your model coefficients 614 | 615 | --- 616 | 617 | # Change in odds ratio: Cons 618 | 619 | .pull-left[ 620 | ```{r ref.label = "odds_ratio_adjusted_axis_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 621 | ``` 622 | ] 623 | 624 | -- 625 | 626 | .pull-right[ 627 | - Not the way we usually describe odds 628 | ] 629 | 630 | --- 631 | 632 | # Change in odds ratio: Cons 633 | 634 | .pull-left[ 635 | ```{r ref.label = "odds_ratio_adjusted_axis_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 636 | ``` 637 | ] 638 | 639 | .pull-right[ 640 | - Not the way we usually describe odds 641 | 642 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 643 | ] 644 | 645 | --- 646 | 647 | # Change in odds ratio: Cons 648 | 649 | .pull-left[ 650 | ```{r ref.label = "odds_ratio_adjusted_axis_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 651 | ``` 652 | ] 653 | 654 | .pull-right[ 655 | - Not the way we usually describe odds 656 | 657 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 658 | 659 | - The unfamiliar format may undo the benefit of using a familiar concept 660 | 661 | {{content}} 662 | ] 663 | 664 | -- 665 | 666 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 667 | 668 | --- 669 | 670 | # Change in odds ratio: Cons 671 | 672 | .pull-left[ 673 | ```{r odds_ratio_adjusted_axis_highlighted_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 674 | odds.ratio.p + 675 | guides(color = guide_legend(nrow = 3)) + 676 | theme(legend.position = "bottom") + 677 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 3), clip = "off") + 678 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = -1.2, xmax = 0.5, 679 | fill = NA, color = "red", size = 2) 680 | ``` 681 | ] 682 | 683 | .pull-right[ 684 | - Not the way we usually describe odds 685 | 686 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 687 | 688 | - The unfamiliar format may undo the benefit of using a familiar concept 689 | 690 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 691 | 692 | - Percent change in odds (300% = triple the odds) might be misinterpreted as a probability 693 | ] 694 | 695 | --- 696 | 697 | # Change in odds ratio: Cons 698 | 699 | .pull-left[ 700 | ```{r ref.label = "odds_ratio_adjusted_axis_highlighted_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 701 | ``` 702 | ] 703 | 704 | .pull-right[ 705 | - Not the way we usually describe odds 706 | 707 | - Usually use integers: "3-to-1" or "2-to-5", not "3" or "0.4" 708 | 709 | - The unfamiliar format may undo the benefit of using a familiar concept 710 | 711 | - Exponentiated coefficients don't represent odds directly; they represent _changes_ in odds 712 | 713 | - Percent change in odds (300% = triple the odds) might be misinterpreted as a probability 714 | 715 | - Now we're pretty far removed from familiar scales 716 | ] 717 | 718 | --- 719 | 720 | # Change in odds ratio: Cons 721 | 722 | .pull-left[ 723 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 724 | log.odds.p + 725 | guides(color = guide_legend(nrow = 3)) + 726 | theme(legend.position = "bottom") + 727 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.5), clip = "off") 728 | ``` 729 | ] 730 | 731 | .pull-right[ 732 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 733 | odds.ratio.p + 734 | guides(color = guide_legend(nrow = 3)) + 735 | theme(legend.position = "bottom") + 736 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 3), clip = "off") 737 | ``` 738 | ] 739 | 740 | --- 741 | 742 | # Change in odds ratio: Cons 743 | 744 | .pull-left[ 745 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3} 746 | log.odds.p + 747 | guides(color = guide_legend(nrow = 3)) + 748 | theme(legend.position = "bottom") + 749 | coord_flip(xlim = c(0.9, 11.1), ylim = c(-1.6, 1.5), clip = "off") + 750 | annotate("rect", ymin = -1.8, ymax = 1.7, xmin = 10.5, xmax = 11.5, 751 | fill = NA, color = "red", size = 2) + 752 | annotate("rect", ymin = -1.8, ymax = 1.7, xmin = 0.5, xmax = 1.5, 753 | fill = NA, color = "red", size = 2) 754 | ``` 755 | ] 756 | 757 | .pull-right[ 758 | ```{r out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 759 | odds.ratio.p + 760 | guides(color = guide_legend(nrow = 3)) + 761 | theme(legend.position = "bottom") + 762 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 3), clip = "off") + 763 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = 10.5, xmax = 11.5, 764 | fill = NA, color = "red", size = 2) + 765 | annotate("rect", ymin = -0.2, ymax = 3.2, xmin = 0.5, xmax = 1.5, 766 | fill = NA, color = "red", size = 2) 767 | ``` 768 | ] 769 | 770 | - The scale is expanded for positive effects and compressed for negative effects 771 | 772 | --- 773 | 774 | class: inverse, center, middle 775 | 776 | # Visualization family 2: 777 | 778 | # Presenting probabilities 779 | 780 | --- 781 | 782 | # Probabilities relative to a baseline 783 | 784 | - Problem with probabilities: change in percentage points depends on baseline starting value 785 | 786 | -- 787 | 788 | - We can choose an appropriate baseline probability, then compute the marginal effect of a predictor given that baseline 789 | 790 | -- 791 | 792 | - Options for baseline: 793 | 794 | -- 795 | 796 | - Model intercept 797 | 798 | -- 799 | 800 | - Observed outcome % in dataset (similar to intercept if continuous predictors are centered and other coefficients aren't too large) 801 | 802 | -- 803 | 804 | - Observed outcome % for a certain group (e.g., students with no tutoring) 805 | 806 | -- 807 | 808 | - Some % that's meaningful in context (e.g., 85% pass rate in typical years) 809 | 810 | --- 811 | 812 | # Probabilities relative to a baseline 813 | 814 | - Baseline probability: inverse logit of the intercept 815 | 816 | $$p_0 = \mbox{logit}^{-1}(\beta_0)$$ 817 | 818 | -- 819 | 820 | - Probability with discrete predictor $i$: inverse logit of intercept + predictor coefficient 821 | 822 | $$p_i = \mbox{logit}^{-1}(\beta_0 + \beta_i)$$ 823 | 824 | -- 825 | 826 | - For a continuous predictor $j$, pick a change in predictor value that makes sense 827 | 828 | -- 829 | 830 | - One standard deviation 831 | 832 | -- 833 | 834 | - A context-specific benchmark (e.g., 1 point for GPA, 100 points on the SAT) 835 | 836 | -- 837 | 838 | $$p_j = \mbox{logit}^{-1}(\beta_0 + \beta_j\Delta x_j)$$ 839 | 840 | -- 841 | 842 | - To show uncertainty, get confidence interval before inverse logit transformation 843 | 844 | --- 845 | 846 | # Probabilities relative to a baseline 847 | 848 | ```{r probability_baseline, include = F, cache = F} 849 | knitr::read_chunk("visual_folder/probability_baseline.R") 850 | ``` 851 | 852 | .pull-left[ 853 | ```{r probability-relative-to-some-baseline-no-arrows, echo = T, fig.show = "hide"} 854 | ``` 855 | ] 856 | 857 | .pull-right[ 858 | ```{r probability_baseline_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 859 | prob.baseline.p + 860 | guides(color = guide_legend(nrow = 3)) + 861 | theme(legend.position = "bottom") + 862 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 1), clip = "off") 863 | ``` 864 | 865 | - (Uncertainty in intercept is not represented here) 866 | ] 867 | 868 | --- 869 | 870 | # Probabilities relative to a baseline: Pros 871 | 872 | .pull-left[ 873 | ```{r ref.label = "probability_baseline_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 874 | ``` 875 | ] 876 | 877 | -- 878 | 879 | .pull-right[ 880 | - Familiar scale: probabilities, expressed as percentages 881 | 882 | {{content}} 883 | ] 884 | 885 | -- 886 | 887 | - Avoids the "percent change" formulation (common but misleading) 888 | 889 | --- 890 | 891 | # Probabilities relative to a baseline: Cons 892 | 893 | .pull-left[ 894 | ```{r probability_baseline_highlighted_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 895 | prob.baseline.p + 896 | guides(color = guide_legend(nrow = 3)) + 897 | theme(legend.position = "bottom") + 898 | coord_flip(xlim = c(0.9, 11.1), ylim = c(0, 1), clip = "off") + 899 | annotate("rect", ymin = invlogit(intercept) - 0.05, 900 | ymax = invlogit(intercept) + 0.05, xmin = 0, xmax = 12, 901 | fill = NA, color = "red", size = 2) 902 | ``` 903 | ] 904 | 905 | -- 906 | 907 | .pull-right[ 908 | - Have to choose a baseline; there may be no "good" choice 909 | 910 | {{content}} 911 | ] 912 | 913 | -- 914 | 915 | - Using the intercept as a baseline chooses reference categories for categorical variables 916 | 917 | --- 918 | 919 | # Probabilities relative to a baseline: Cons 920 | 921 | .pull-left[ 922 | ```{r ref.label = "probability_baseline_highlighted_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 923 | ``` 924 | ] 925 | 926 | .pull-right[ 927 | - Have to choose a baseline; there may be no "good" choice 928 | 929 | - Using the intercept as a baseline chooses reference categories for categorical variables 930 | 931 | - Students who don't use Macs, don't wear glasses, etc. 932 | ] 933 | 934 | --- 935 | 936 | # Probabilities relative to a baseline: Cons 937 | 938 | .pull-left[ 939 | ```{r ref.label = "probability_baseline_highlighted_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 940 | ``` 941 | ] 942 | 943 | .pull-right[ 944 | - Have to choose a baseline; there may be no "good" choice 945 | 946 | - Using the intercept as a baseline chooses reference categories for categorical variables 947 | 948 | - Students who don't use Macs, don't wear glasses, etc. 949 | 950 | - Not an appropriate choice for all datasets 951 | 952 | {{content}} 953 | ] 954 | 955 | -- 956 | 957 | - Doesn't show full range of possible effects at different baselines 958 | 959 | --- 960 | 961 | # Probabilities relative to a baseline: Arrows 962 | 963 | .pull-left[ 964 | ```{r probability-relative-to-some-baseline-with-arrows, echo = T, fig.show = "hide"} 965 | ``` 966 | ] 967 | 968 | .pull-right[ 969 | ```{r probability_baseline_arrows_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 970 | prob.baseline.arrows.p + 971 | guides(color = guide_legend(nrow = 3)) + 972 | theme(legend.position = "bottom") + 973 | coord_cartesian(xlim = c(0, 1), ylim = c(0.9, 11.1), clip = "off") 974 | ``` 975 | ] 976 | 977 | --- 978 | 979 | # Probabilities relative to a baseline: Arrows 980 | 981 | .pull-left[ 982 | ```{r ref.label = "probability_baseline_arrows_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 983 | ``` 984 | ] 985 | 986 | -- 987 | 988 | .pull-right[ 989 | - Emphasizes direction of effect 990 | 991 | {{content}} 992 | ] 993 | 994 | -- 995 | 996 | - Doesn't show uncertainty around estimates 997 | 998 | {{content}} 999 | 1000 | -- 1001 | 1002 | - Strong causal implications 1003 | 1004 | --- 1005 | 1006 | # Multiple baselines by group 1007 | 1008 | - Instead of one baseline probability, why not several? 1009 | 1010 | -- 1011 | 1012 | - Example: show effect of $i$ for each level of categorical variable $j$ 1013 | 1014 | -- 1015 | 1016 | $$p_{j1} = \mbox{logit}^{-1}(\beta_0 + \beta_{j1})$$ 1017 | 1018 | -- 1019 | 1020 | $$p_{j1 + i} = \mbox{logit}^{-1}(\beta_0 + \beta_{j1} + \beta_i)$$ 1021 | 1022 | --- 1023 | 1024 | # Multiple baselines by group 1025 | 1026 | ```{r probability_group, include = F} 1027 | knitr::read_chunk("visual_folder/probability_group.R") 1028 | ``` 1029 | 1030 | .pull-left[ 1031 | ```{r probability-relative-to-some-baseline-and-group-no-arrows, echo = T, fig.show = "hide"} 1032 | ``` 1033 | ] 1034 | 1035 | .pull-right[ 1036 | ```{r probability_group_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.4, fig.retina = 6} 1037 | prob.group.p + 1038 | guides(color = guide_legend(nrow = 3)) + 1039 | theme(legend.position = "bottom") + 1040 | coord_flip(xlim = c(0.9, 8.1), ylim = c(0, 1), clip = "off") 1041 | ``` 1042 | ] 1043 | 1044 | --- 1045 | 1046 | # Multiple baselines by group: Pros 1047 | 1048 | .pull-left[ 1049 | ```{r ref.label = "probability_group_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1050 | ``` 1051 | ] 1052 | 1053 | -- 1054 | 1055 | .pull-right[ 1056 | - Emphasizes that the baseline we show is a _choice_ 1057 | ] 1058 | 1059 | --- 1060 | 1061 | # Multiple baselines by group: Pros 1062 | 1063 | .pull-left[ 1064 | ```{r probability_group_highlighted_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1065 | prob.group.p + 1066 | guides(color = guide_legend(nrow = 3)) + 1067 | theme(legend.position = "bottom") + 1068 | coord_flip(xlim = c(0.9, 8.1), ylim = c(0, 1), clip = "off") + 1069 | annotate("rect", xmin = 7.5, xmax = 8.5, ymin = 0.01, ymax = 0.99, 1070 | fill = NA, color = "red", size = 1) 1071 | ``` 1072 | ] 1073 | 1074 | .pull-right[ 1075 | - Emphasizes that the baseline we show is a _choice_ 1076 | 1077 | - Honors differences among groups 1078 | ] 1079 | 1080 | --- 1081 | 1082 | # Multiple baselines by group: Pros 1083 | 1084 | .pull-left[ 1085 | ```{r ref.label = "probability_group_highlighted_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1086 | ``` 1087 | ] 1088 | 1089 | .pull-right[ 1090 | - Emphasizes that the baseline we show is a _choice_ 1091 | 1092 | - Honors differences among groups 1093 | 1094 | - Effect of GPA is larger for fish owners than for dog owners 1095 | ] 1096 | 1097 | --- 1098 | 1099 | # Multiple baselines by group: Pros 1100 | 1101 | .pull-left[ 1102 | ```{r ref.label = "probability_group_highlighted_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1103 | ``` 1104 | ] 1105 | 1106 | .pull-right[ 1107 | - Emphasizes that the baseline we show is a _choice_ 1108 | 1109 | - Honors differences among groups 1110 | 1111 | - Effect of GPA is larger for fish owners than for dog owners 1112 | 1113 | - Here, this is purely because of fish owners' lower baseline 1114 | ] 1115 | 1116 | --- 1117 | 1118 | # Multiple baselines by group: Pros 1119 | 1120 | .pull-left[ 1121 | ```{r ref.label = "probability_group_highlighted_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1122 | ``` 1123 | ] 1124 | 1125 | .pull-right[ 1126 | - Emphasizes that the baseline we show is a _choice_ 1127 | 1128 | - Honors differences among groups 1129 | 1130 | - Effect of GPA is larger for fish owners than for dog owners 1131 | 1132 | - Here, this is purely because of fish owners' lower baseline 1133 | 1134 | - But we could also show the effects of an interaction term in the model 1135 | 1136 | {{content}} 1137 | ] 1138 | 1139 | -- 1140 | 1141 | - We can use arrows here as well 1142 | 1143 | --- 1144 | 1145 | # Multiple baselines by group: Cons 1146 | 1147 | .pull-left[ 1148 | ```{r ref.label = "probability_group_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1149 | ``` 1150 | ] 1151 | 1152 | -- 1153 | 1154 | .pull-right[ 1155 | - Still have to choose baselines by group 1156 | 1157 | {{content}} 1158 | ] 1159 | 1160 | -- 1161 | 1162 | - May suggest essentializing interpretations of groups 1163 | 1164 | {{content}} 1165 | 1166 | -- 1167 | 1168 | - Cluttered 1169 | 1170 | --- 1171 | 1172 | # Banana graphs 1173 | 1174 | - We can overcome the baseline-choosing problem by iterating across every baseline 1175 | 1176 | -- 1177 | 1178 | - For example: 1179 | 1180 | -- 1181 | 1182 | - Start with every possible probability of passing Balloon Animal-Making 201, from 0% to 100% (at sufficiently small intervals) 1183 | 1184 | -- 1185 | 1186 | - For each probability, add the effect of having a pet fish 1187 | 1188 | -- 1189 | 1190 | $$p_f = \mbox{logit}^{-1}(\mbox{logit}(p_0) + \beta_f)$$ 1191 | 1192 | --- 1193 | # Banana graphs 1194 | 1195 | ```{r banana_graphs, include = F} 1196 | knitr::read_chunk("visual_folder/banana_graphs.R") 1197 | ``` 1198 | 1199 | .pull-left[ 1200 | ```{r banana-graph, echo = T, fig.show = "hide"} 1201 | ``` 1202 | ] 1203 | 1204 | .pull-right[ 1205 | ```{r banana_graph_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1206 | banana.p + 1207 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") 1208 | ``` 1209 | ] 1210 | 1211 | --- 1212 | 1213 | # Banana graphs 1214 | 1215 | .pull-left[ 1216 | ```{r ref.label = "banana_graph_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1217 | ``` 1218 | ] 1219 | 1220 | -- 1221 | 1222 | .pull-right[ 1223 | - **x-axis:** baseline probability 1224 | 1225 | {{content}} 1226 | ] 1227 | 1228 | -- 1229 | 1230 | - **y-axis:** probability with effect of having a pet fish 1231 | 1232 | {{content}} 1233 | 1234 | -- 1235 | 1236 | - Solid line provides a reference (no effect) 1237 | 1238 | {{content}} 1239 | 1240 | -- 1241 | 1242 | - Positive effects above the line; negative effects below the line; no effect on the line 1243 | 1244 | --- 1245 | # Banana graphs 1246 | 1247 | ```{r banana_graph_highlighted_plot, out.width = "50%", fig.asp = 1.3, fig.retina = 6} 1248 | banana.p + 1249 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") + 1250 | annotate("segment", x = 0.4, xend = 0.4, y = 0.4, 1251 | yend = invlogit(logit(0.4) + 1252 | coefs.df$est[coefs.df$parameter == "pet.typefish"]), 1253 | color = "black", size = 0.5, 1254 | arrow = arrow(type = "closed", length = unit(0.02, units = "npc"))) + 1255 | annotate("text", x = 0.1, y = 0.7, size = 2.90, hjust = 0, 1256 | label = str_wrap(paste("Some student who does not own a pet fish has a 40% chance of passing (x-axis value).", 1257 | " However, if that same student did own a pet fish,", 1258 | " their predicted probability of passing would be ", 1259 | round(invlogit(logit(0.4) + 1260 | coefs.df$est[coefs.df$parameter == "pet.typefish"]) * 100), 1261 | "% (y-axis value).", 1262 | sep = ""), 1263 | 30)) 1264 | ``` 1265 | 1266 | --- 1267 | 1268 | # Banana graphs 1269 | 1270 | .pull-left[ 1271 | ```{r banana-graph-multiple, echo = T, fig.show = "hide"} 1272 | ``` 1273 | ] 1274 | 1275 | .pull-right[ 1276 | ```{r banana_graph_multiple_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1277 | banana.multiple.p + 1278 | coord_cartesian(xlim = c(0, 1), ylim = c(0, 1), clip = "off") 1279 | ``` 1280 | ] 1281 | 1282 | --- 1283 | 1284 | # Banana graphs: Pros 1285 | 1286 | .pull-left[ 1287 | ```{r ref.label = "banana_graph_multiple_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1288 | ``` 1289 | ] 1290 | 1291 | -- 1292 | 1293 | .pull-right[ 1294 | - Do not have to pick and choose a baseline 1295 | 1296 | {{content}} 1297 | ] 1298 | 1299 | -- 1300 | 1301 | - Show the whole range of predicted probabilities 1302 | 1303 | --- 1304 | 1305 | # Banana graphs: Cons 1306 | 1307 | .pull-left[ 1308 | ```{r ref.label = "banana_graph_multiple_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1309 | ``` 1310 | ] 1311 | 1312 | -- 1313 | 1314 | .pull-right[ 1315 | - Can take up quite a bit of space 1316 | 1317 | {{content}} 1318 | ] 1319 | 1320 | -- 1321 | 1322 | - May be initially difficult to understand 1323 | 1324 | {{content}} 1325 | 1326 | -- 1327 | 1328 | - Predictor variables are in separate graphs: hard to compare 1329 | 1330 | --- 1331 | 1332 | class: inverse, center, middle 1333 | 1334 | # Visualization family 3: 1335 | 1336 | # Counterfactual counts 1337 | 1338 | --- 1339 | 1340 | # Extra successes 1341 | 1342 | - Sometimes stakeholders are interested in **the number of times something happens (or doesn't happen)** 1343 | 1344 | -- 1345 | 1346 | - Example: stakeholders want to assess the impact of tutoring on pass rates in Balloon Animal-Making 201 1347 | 1348 | -- 1349 | 1350 | - They're interested not just in _whether_ tutoring helps students, but _how much_ it helps them 1351 | 1352 | -- 1353 | 1354 | - In our dataset, `r format(sum(df$tutoring), big.mark = ",")` students received tutoring; of those, `r format(sum(df$tutoring & df$passed), big.mark = ",")` passed the class 1355 | 1356 | -- 1357 | 1358 | - Suppose those students had _not_ received tutoring; in that case, how many would have passed? 1359 | 1360 | -- 1361 | 1362 | - In other words, how many "extra" passes did we get because of tutoring? 1363 | 1364 | --- 1365 | 1366 | # Extra successes 1367 | 1368 | - To get a point estimate: 1369 | 1370 | -- 1371 | 1372 | - Take all students who received tutoring 1373 | 1374 | -- 1375 | 1376 | - Set `tutoring` to `FALSE` instead of `TRUE` 1377 | 1378 | -- 1379 | 1380 | - Use the model to make (counterfactual) predictions for the revised dataset 1381 | 1382 | -- 1383 | 1384 | - Count predicted counterfactual passes; compare to the actual number of passes 1385 | 1386 | -- 1387 | 1388 | - We can get confidence intervals by simulating many sets of outcomes and aggregating over them. 1389 | 1390 | --- 1391 | 1392 | # Extra successes 1393 | 1394 | ```{r counterfactuals, include = F} 1395 | knitr::read_chunk("visual_folder/counterfactuals.R") 1396 | ``` 1397 | 1398 | .pull-left[ 1399 | ```{r extra-passes, echo = T, fig.show = "hide"} 1400 | ``` 1401 | ] 1402 | 1403 | .pull-right[ 1404 | ```{r extra_passes_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.4, fig.retina = 6} 1405 | extra.p 1406 | ``` 1407 | ] 1408 | 1409 | --- 1410 | 1411 | # Extra successes: Pros 1412 | 1413 | .pull-left[ 1414 | ```{r ref.label = "extra_passes_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1415 | ``` 1416 | ] 1417 | 1418 | -- 1419 | 1420 | .pull-right[ 1421 | - Counts have a straightforward interpretation 1422 | 1423 | {{content}} 1424 | ] 1425 | 1426 | -- 1427 | 1428 | - Natural baseline: account for other characteristics of your population (e.g., number of fish owners) 1429 | 1430 | --- 1431 | 1432 | # Extra successes: Cons 1433 | 1434 | .pull-left[ 1435 | ```{r ref.label = "extra_passes_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1436 | ``` 1437 | ] 1438 | 1439 | -- 1440 | 1441 | .pull-right[ 1442 | - "Number of simulations" may be hard to explain 1443 | 1444 | {{content}} 1445 | ] 1446 | 1447 | -- 1448 | 1449 | - Assumes that the counterfactual makes sense 1450 | 1451 | {{content}} 1452 | 1453 | -- 1454 | 1455 | - Strong causal interpretation 1456 | 1457 | --- 1458 | 1459 | # Extra successes by group 1460 | 1461 | - Your stakeholders may be interested in different effects by group 1462 | 1463 | -- 1464 | 1465 | - We can summarize counterfactuals for separate groups 1466 | 1467 | --- 1468 | 1469 | # Extra successes by group 1470 | 1471 | .pull-left[ 1472 | ```{r extra-passes-by-group, echo = T, fig.show = "hide"} 1473 | ``` 1474 | ] 1475 | 1476 | .pull-right[ 1477 | ```{r extra_passes_group_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.4, fig.retina = 6} 1478 | extra.group.p 1479 | ``` 1480 | ] 1481 | 1482 | --- 1483 | 1484 | # Extra successes by group: Pros 1485 | 1486 | .pull-left[ 1487 | ```{r ref.label = "extra_passes_group_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1488 | ``` 1489 | ] 1490 | 1491 | -- 1492 | 1493 | .pull-right[ 1494 | - Avoids a scale with number of simulations; focus is on range of predictions 1495 | 1496 | {{content}} 1497 | ] 1498 | 1499 | -- 1500 | 1501 | - Shows differences by group 1502 | 1503 | {{content}} 1504 | 1505 | -- 1506 | 1507 | - Interaction terms in the model would be incorporated automatically 1508 | 1509 | --- 1510 | 1511 | # Extra successes by group: Cons 1512 | 1513 | .pull-left[ 1514 | ```{r ref.label = "extra_passes_group_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1515 | ``` 1516 | ] 1517 | 1518 | -- 1519 | 1520 | .pull-right[ 1521 | - Doesn't show how absolute numbers depend on group size 1522 | 1523 | {{content}} 1524 | ] 1525 | 1526 | -- 1527 | 1528 | - Tutoring actually has a _larger_ percentage point effect for fish owners (because of the lower baseline), but the group is small 1529 | 1530 | {{content}} 1531 | 1532 | -- 1533 | 1534 | - (Your audience may care about counts, percentages, or both) 1535 | 1536 | --- 1537 | 1538 | # Potential successes compared to group size 1539 | 1540 | - Attempt to show _both_ the effect size for each group _and_ the overall size of that group 1541 | 1542 | -- 1543 | 1544 | - Here, we switch the direction of the counterfactual 1545 | 1546 | -- 1547 | 1548 | - Start with untutored students 1549 | 1550 | -- 1551 | 1552 | - How many would have passed with tutoring? 1553 | 1554 | -- 1555 | 1556 | - We think this emphasizes the benefits of tutoring more clearly in this graph 1557 | 1558 | -- 1559 | 1560 | - Either direction is possible; do what makes sense in your context! 1561 | 1562 | --- 1563 | 1564 | # Potential successes compared to group size 1565 | 1566 | .pull-left[ 1567 | ```{r potential-passes-by-group, echo = T, fig.show = "hide"} 1568 | ``` 1569 | ] 1570 | 1571 | .pull-right[ 1572 | ```{r potential_passes_group_plot, out.width = "100%", fig.width = 4.4, fig.asp = 1.4, fig.retina = 6} 1573 | potential.group.p + 1574 | theme(legend.position = "bottom") 1575 | ``` 1576 | ] 1577 | 1578 | --- 1579 | 1580 | # Potential successes compared to group size 1581 | 1582 | .pull-left[ 1583 | ```{r ref.label = "potential_passes_group_plot", out.width = "100%", fig.width = 4.4, fig.asp = 1.3, fig.retina = 6} 1584 | ``` 1585 | ] 1586 | 1587 | -- 1588 | 1589 | .pull-right[ 1590 | - Acknowledges different group sizes: puts absolute numbers in context 1591 | 1592 | {{content}} 1593 | ] 1594 | 1595 | -- 1596 | 1597 | - But small groups are squished at the bottom of the scale (hard to see) 1598 | 1599 | --- 1600 | 1601 | # Conclusion 1602 | 1603 | - There is no right or wrong way, only better and worse ways for a particular project, so get creative! 1604 | 1605 | -- 1606 | 1607 | - Knowing your stakeholders as well as the context and purpose of your research should be your guides to determine which visualization is most appropriate 1608 | 1609 | -- 1610 | 1611 | - Use colors, the layout, and annotations to your advantage 1612 | 1613 | -- 1614 | 1615 | - Share your ideas with others 1616 | 1617 | --- 1618 | 1619 | class: inverse, center, middle 1620 | 1621 | # Thank you! 1622 | -------------------------------------------------------------------------------- /RUG_presentation/blinking_meme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikcaw/visualizing-logistic-regression/a97dafc9fab6cedce41b0a41ea88f1baece8474e/RUG_presentation/blinking_meme.jpg -------------------------------------------------------------------------------- /RUG_presentation/xaringan-themer.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------- 2 | * 3 | * !! This file was generated by xaringanthemer !! 4 | * 5 | * Changes made to this file directly will be overwritten 6 | * if you used xaringanthemer in your xaringan slides Rmd 7 | * 8 | * Issues or likes? 9 | * - https://github.com/gadenbuie/xaringanthemer 10 | * - https://www.garrickadenbuie.com 11 | * 12 | * Need help? Try: 13 | * - vignette(package = "xaringanthemer") 14 | * - ?xaringanthemer::write_xaringan_theme 15 | * - xaringan wiki: https://github.com/yihui/xaringan/wiki 16 | * - remarkjs wiki: https://github.com/gnab/remark/wiki 17 | * 18 | * ------------------------------------------------------- */ 19 | @import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic); 20 | @import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz); 21 | @import url(https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700); 22 | 23 | 24 | body { 25 | font-family: 'Droid Serif', 'Palatino Linotype', 'Book Antiqua', Palatino, 'Microsoft YaHei', 'Songti SC', serif; 26 | font-weight: normal; 27 | color: #000000; 28 | } 29 | h1, h2, h3 { 30 | font-family: 'Yanone Kaffeesatz'; 31 | font-weight: normal; 32 | } 33 | .remark-slide-content { 34 | background-color: #FFFFFF; 35 | font-size: 20px; 36 | 37 | 38 | 39 | padding: 1em 4em 1em 4em; 40 | } 41 | .remark-slide-content h1 { 42 | font-size: 55px; 43 | } 44 | .remark-slide-content h2 { 45 | font-size: 45px; 46 | } 47 | .remark-slide-content h3 { 48 | font-size: 35px; 49 | } 50 | .remark-code, .remark-inline-code { 51 | font-family: 'Source Code Pro', 'Lucida Console', Monaco, monospace; 52 | } 53 | .remark-code { 54 | font-size: 0.9em; 55 | } 56 | .remark-inline-code { 57 | font-size: 1em; 58 | color: #00A8E1; 59 | 60 | 61 | } 62 | .remark-slide-number { 63 | color: #00A8E1; 64 | opacity: 1; 65 | font-size: 0.9em; 66 | } 67 | strong{color:#00A8E1;} 68 | a, a > code { 69 | color: #00A8E1; 70 | text-decoration: none; 71 | } 72 | .footnote { 73 | 74 | position: absolute; 75 | bottom: 3em; 76 | padding-right: 4em; 77 | font-size: 0.9em; 78 | } 79 | .remark-code-line-highlighted { 80 | background-color: rgba(255,255,0,0.5); 81 | } 82 | .inverse { 83 | background-color: #272822; 84 | color: #d6d6d6; 85 | text-shadow: 0 0 20px #333; 86 | } 87 | .inverse h1, .inverse h2, .inverse h3 { 88 | color: #f3f3f3; 89 | } 90 | .title-slide .remark-slide-number { 91 | display: none; 92 | } 93 | /* Two-column layout */ 94 | .left-column { 95 | width: 20%; 96 | height: 92%; 97 | float: left; 98 | } 99 | .left-column h2, .left-column h3 { 100 | color: #00A8E199; 101 | } 102 | .left-column h2:last-of-type, .left-column h3:last-child { 103 | color: #00A8E1; 104 | } 105 | .right-column { 106 | width: 75%; 107 | float: right; 108 | padding-top: 1em; 109 | } 110 | .pull-left { 111 | float: left; 112 | width: 47%; 113 | } 114 | .pull-right { 115 | float: right; 116 | width: 47%; 117 | } 118 | .pull-right ~ * { 119 | clear: both; 120 | } 121 | img, video, iframe { 122 | max-width: 100%; 123 | } 124 | blockquote { 125 | border-left: solid 5px #FFEFBD80; 126 | padding-left: 1em; 127 | } 128 | .remark-slide table { 129 | margin: auto; 130 | border-top: 1px solid #666; 131 | border-bottom: 1px solid #666; 132 | } 133 | .remark-slide table thead th { border-bottom: 1px solid #ddd; } 134 | th, td { padding: 5px; } 135 | .remark-slide thead, .remark-slide tfoot, .remark-slide tr:nth-child(even) { background: #EEEEEE } 136 | table.dataTable tbody { 137 | background-color: #FFFFFF; 138 | color: #000000; 139 | } 140 | table.dataTable.display tbody tr.odd { 141 | background-color: #FFFFFF; 142 | } 143 | table.dataTable.display tbody tr.even { 144 | background-color: #EEEEEE; 145 | } 146 | table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { 147 | background-color: rgba(255, 255, 255, 0.5); 148 | } 149 | .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate { 150 | color: #000000; 151 | } 152 | .dataTables_wrapper .dataTables_paginate .paginate_button { 153 | color: #000000 !important; 154 | } 155 | 156 | @page { margin: 0; } 157 | @media print { 158 | .remark-slide-scaler { 159 | width: 100% !important; 160 | height: 100% !important; 161 | transform: scale(1) !important; 162 | top: 0 !important; 163 | left: 0 !important; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /R_code/fit_model.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | 3 | ## ---- load-data ---- 4 | 5 | library(tidyverse) 6 | df = read.csv("data/course_outcomes.csv", header = T, stringsAsFactors = F) 7 | 8 | ## ---- prepare-data-continuous ---- 9 | 10 | df = df %>% 11 | mutate(cs.prior.gpa = (prior.gpa - mean(prior.gpa)) / sd(prior.gpa), 12 | cs.height = (height - mean(height)) / sd(height)) 13 | 14 | ## ---- prepare-data-categorical ---- 15 | 16 | df = df %>% 17 | mutate(pet.type = fct_relevel(pet.type, "none", "dog", "cat", "fish"), 18 | favorite.color = fct_relevel(favorite.color, "blue", "red", "green", 19 | "orange")) 20 | 21 | ## ---- model ---- 22 | 23 | library(lme4) 24 | pass.m = glm(passed ~ mac + glasses + pet.type + favorite.color + cs.prior.gpa + 25 | cs.height + tutoring, 26 | data = df, family = binomial(link = "logit")) 27 | summary(pass.m)$coefficients 28 | 29 | ## ---- get-coefficients ---- 30 | 31 | coefs.df = summary(pass.m)$coefficients %>% 32 | data.frame() %>% 33 | rownames_to_column("parameter") %>% 34 | mutate(pretty.parameter = 35 | case_when(parameter == "(Intercept)" ~ "Intercept", 36 | grepl("TRUE$", parameter) ~ 37 | str_to_title(gsub("TRUE", "", parameter)), 38 | grepl("pet\\.type", parameter) ~ 39 | paste("Pet:", str_to_title(gsub("pet\\.type", "", parameter))), 40 | grepl("favorite\\.color", parameter) ~ 41 | paste("Favorite color:", 42 | str_to_title(gsub("favorite\\.color", "", parameter))), 43 | parameter == "cs.prior.gpa" ~ 44 | paste("Prior GPA\n(", round(sd(df$prior.gpa), 1), 45 | "-pt increase)", sep = ""), 46 | parameter == "cs.height" ~ 47 | paste("Height\n(", round(sd(df$height), 1), 48 | "-in increase)", sep = ""))) %>% 49 | dplyr::select(parameter, pretty.parameter, est = Estimate, se = Std..Error, 50 | z = z.value, p = Pr...z..) 51 | -------------------------------------------------------------------------------- /R_code/simulate_data.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(logitnorm) 3 | 4 | n.students = 5000 5 | parameters = list(intercept = 1.4, mac = 0, glasses = 0.3, pet.typedog = -0.1, 6 | pet.typecat = 0.3, pet.typefish = -1, 7 | favorite.colorred = 0, favorite.colorgreen = -0.3, 8 | favorite.colororange = -0.1, cs.prior.gpa = 1, 9 | cs.height = -0.2, tutoring = 0.18) 10 | 11 | df = data.frame(id = 1:n.students) %>% 12 | mutate(mac = runif(n()) < 0.3, 13 | glasses = runif(n()) < 0.4, 14 | pet.rand = runif(n()), 15 | pet.type = case_when(pet.rand < 0.4 ~ "none", 16 | pet.rand < 0.8 ~ "dog", 17 | pet.rand < 0.95 ~ "cat", 18 | T ~ "fish"), 19 | pet.type = fct_relevel(pet.type, "none", "dog", "cat", "fish"), 20 | color.rand = runif(n()), 21 | favorite.color = case_when(color.rand < 0.4 ~ "blue", 22 | color.rand < 0.7 ~ "red", 23 | color.rand < 0.9 ~ "green", 24 | T ~ "orange"), 25 | favorite.color = fct_relevel(favorite.color, "blue", "red", "green", 26 | "orange"), 27 | prior.gpa = round(rbeta(n(), 5, 1) * 4, 2), 28 | cs.prior.gpa = (prior.gpa - mean(prior.gpa)) / sd(prior.gpa), 29 | height = round(rnorm(n(), 66, 3)), 30 | cs.height = (height - mean(height)) / sd(height), 31 | tutoring = runif(n()) < 0.5) %>% 32 | dplyr::select(id, mac, glasses, pet.type, favorite.color, prior.gpa, 33 | cs.prior.gpa, height, cs.height, tutoring) 34 | df$passed = invlogit((model.matrix(~ mac + glasses + pet.type + favorite.color + 35 | cs.prior.gpa + cs.height + tutoring, 36 | data = df) %*% unlist(parameters))[,1]) 37 | df$passed = df$passed > runif(nrow(df)) 38 | 39 | write.csv(df %>% 40 | dplyr::select(id, mac, glasses, pet.type, favorite.color, 41 | prior.gpa, height, tutoring, passed), 42 | "data/course_outcomes.csv", row.names = F) 43 | -------------------------------------------------------------------------------- /nomogram_explanation.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Nomograms" 3 | output: 4 | html_document: 5 | code_folding: hide 6 | toc: true 7 | toc_float: true 8 | --- 9 | 10 | ```{r setup, include = F, warning = F} 11 | library(dplyr) 12 | library(ggplot2) 13 | library(tidyverse) 14 | library(rms) 15 | library(ggrepel) 16 | library(knitr) 17 | theme_set(theme_bw()) 18 | opts_chunk$set(echo = T, message = F, warning = F, error = F, fig.retina = 3, 19 | fig.align = "center", fig.width = 6, fig.asp = 0.7) 20 | ``` 21 | 22 | 23 | * *I'm thinking that maybe this should go after the banana graphs? Or at least somewhere in the probability section.* 24 | * *Is using `good.color` for the arrow colors okay?* 25 | 26 | 27 | # Nomograms 28 | 29 | The nomogram is graphical tool which can be used for quick computations of probability. Using our Balloon-Animal Making students, the nomogram would be useful for a stakeholder who may need to calculate a specific student's probability of passing the class. For example, your stakeholder may need to know what the probability of passing Balloon-Animal Making is for a student with a 3.5 prior GPA, height of 5 ft 4, owns a pet fish, favorite color is green, did not attend tutoring, wears glasses, and uses a Mac. 30 | 31 | Additionally, nomograms are not only a helpful computation tool, but they can help audiences visualize continuous predictors better than any of the previous visuals. So far, for each discrete predictor we could present the point estimate for each of the non-reference categories. For the continuous variables, we could only present one point estimate even though a continuous variable contains a *range* of values. For example, for each of the visuals so far, the discrete predictor "Favorite color" had a y-axis value for Red, Orange, and Green, but the continuous variable "Prior GPA" did not have a y-axis tick for "Prior GPA" equal to 3.86, 2.37, 1.98, 4.00, etc. Nomograms, however, can show the whole range of values in your dataset for a continuous predictors. This is helpful, particularly for audience members who might need help making sense of their predictor within the context of the other predictors. 32 | 33 | ```{r colors, include = F} 34 | knitr::read_chunk("visual_folder/colors.R") 35 | ``` 36 | ```{r color-palette, include = F} 37 | ``` 38 | ```{r fit_model, include = F} 39 | knitr::read_chunk("R_code/fit_model.R") 40 | ``` 41 | ```{r load-data, include = F} 42 | ``` 43 | ```{r prepare-data-continuous, include = F} 44 | ``` 45 | ```{r prepare-data-categorical, include = F} 46 | ``` 47 | ```{r nomograms, include = F} 48 | knitr::read_chunk("visual_folder/nomograms.R") 49 | ``` 50 | 51 | The way that the nomogram works is that each predictor is assigned a range of points, and each value within a predictor corresponds to a point. The points assigned to each value should reflect the amount of contribution a particular predictor's value has on the outcome of interest's probability. 52 | 53 | ```{r create_logit_model_with_rms, include = F} 54 | ``` 55 | 56 | ```{r create_nomogram_using_rms, include = F} 57 | ``` 58 | 59 | ```{r create_dataframe_for_nomogram_values, include = F} 60 | ``` 61 | 62 | ```{r create_nomogram, fig.asp = 0.8, fig.width = 11} 63 | ``` 64 | 65 | For a user to compute the probability, they would first to need to identify the point associated with each value for each predictor. In the figure below, one blue arrow shows that a student who has a pet dog will correspond with roughly 13 points, while the other blue arrow shows that a student who has a GPA of roughly 3.4 corresponds with 80 points. 66 | 67 | ```{r nomogram_identify_points, fig.asp = 0.8, fig.width = 11} 68 | ``` 69 | 70 | After identifying the number of points for each predictor, the next step is to sum all of the points obtained from each predictor's value of interest to obtain the total points. After obtaining the total point value, we can identify the probability value which aligns with the total point value by drawing a straight line from the total points line to the parallel probability line. In the example below, if we found that a student's total points equaled 80 points then the probability of passing Balloon-Animal Making would be roughly 28%. 71 | 72 | ```{r nomogram_identify_probability, fig.asp = 0.8, fig.width = 11} 73 | ``` 74 | 75 | This visual cannot show which predictors are significant as seamlessly as the previous visuals. One option to address this issue would be to color the axis ticks based on significance, but this would likely cause more visual clutter than aide. Another option would be to change the shape of each x-axis tick label which is significant, but without color and with how small the tick label may be, the significance will not be as visibly obvious as the previous visuals. Another visual element nomograms cannot show is a confidence interval. Furthermore, much like the banana graphs, this visual will likely require some explanation if the audience member is not familiar with nomograms. Lastly, depending on your comfort level in programming, creating nomograms without a package may be time consuming. Although the `rms` package has a `nomogram` function which can easily be used to create nomograms, adjusting and customizing the default plot from this function may also be time consuming. For example, we centered and standardized the continuous predictors for the model, which means that the `nomogram` will display the center and standardized variable which your audience member will likely not be able to meaningfully interpret. 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /visual_folder/banana_graphs.R: -------------------------------------------------------------------------------- 1 | ## ---- banana-graph ---- 2 | 3 | est = coefs.df$est[coefs.df$parameter == "pet.typefish"] 4 | se = coefs.df$se[coefs.df$parameter == "pet.typefish"] 5 | effect.color = case_when(coefs.df$p[coefs.df$parameter == "pet.typefish"] > 0.05 ~ neutral.color, 6 | est > 0 ~ good.color, 7 | T ~ bad.color) 8 | banana.p = data.frame(x = seq(0.01, 0.99, 0.01), 9 | upper.95 = 0.975, 10 | upper.50 = 0.75, 11 | median = 0.5, 12 | lower.50 = 0.25, 13 | lower.95 = 0.025) %>% 14 | mutate(across(matches("median|upper|lower"), #<< 15 | function(q) { #<< 16 | current.x = get("x") #<< 17 | invlogit(logit(current.x) + #<< 18 | est + #<< 19 | (qnorm(q) * se)) #<< 20 | })) %>% #<< 21 | ggplot(aes(x = x, group = 1)) + 22 | geom_segment(x = 0, xend = 1, y = 0, yend = 1) + 23 | geom_ribbon(aes(ymin = lower.95, ymax = upper.95), 24 | fill = effect.color, alpha = 0.2) + 25 | geom_ribbon(aes(ymin = lower.50, ymax = upper.50), 26 | fill = effect.color, alpha = 0.4) + 27 | geom_line(aes(y = median), color = effect.color) + 28 | scale_x_continuous(labels = scales::percent_format()) + 29 | scale_y_continuous(labels = scales::percent_format()) + 30 | labs(x = "Baseline probablity of passing", 31 | y = "Probability of passing with effect", 32 | title = gsub("\n", " ", 33 | coefs.df$pretty.parameter[coefs.df$parameter == "pet.typefish"]), 34 | subtitle = "Estimated relationship to probability of passing") 35 | 36 | ## ---- banana-graph-multiple ---- 37 | 38 | banana.multiple.p = 39 | expand.grid(x = seq(0.01, 0.99, 0.01), #<< 40 | pet = c("fish", "dog", "cat")) %>% #<< 41 | mutate(pet = paste("pet.type", pet, sep = ""), 42 | upper.95 = 0.975, 43 | upper.50 = 0.75, 44 | median = 0.5, 45 | lower.50 = 0.25, 46 | lower.95 = 0.025) %>% 47 | inner_join(coefs.df, #<< 48 | by = c("pet" = "parameter")) %>% #<< 49 | mutate(across(matches("median|upper|lower"), 50 | function(q) { 51 | current.x = get("x") 52 | invlogit(logit(current.x) + est + (qnorm(q) * se)) 53 | })) %>% 54 | mutate(effect.color = case_when(p > 0.05 ~ neutral.color, 55 | est > 0 ~ good.color, 56 | T ~ bad.color)) %>% 57 | ggplot(aes(x = x, color = effect.color, fill = effect.color, group = 1)) + 58 | geom_segment(x = 0, xend = 1, y = 0, yend = 1, color = "black") + 59 | geom_ribbon(aes(ymin = lower.95, ymax = upper.95), color = NA, alpha = 0.2) + 60 | geom_ribbon(aes(ymin = lower.50, ymax = upper.50), color = NA, alpha = 0.4) + 61 | geom_line(aes(y = median)) + 62 | scale_x_continuous(labels = scales::percent_format()) + 63 | scale_y_continuous(labels = scales::percent_format()) + 64 | scale_color_identity() + 65 | scale_fill_identity() + 66 | labs(x = "Baseline probablity of passing", 67 | y = "Probability of passing with effect", 68 | title = "Estimated relationship to\nprobability of passing") + 69 | facet_wrap(~ pretty.parameter, ncol = 1) #<< 70 | -------------------------------------------------------------------------------- /visual_folder/colors.R: -------------------------------------------------------------------------------- 1 | ## ---- color-palette ---- 2 | good.color = "#0571B0" 3 | neutral.color = "gray" 4 | bad.color = "#CA0020" 5 | -------------------------------------------------------------------------------- /visual_folder/counterfactuals.R: -------------------------------------------------------------------------------- 1 | ## ---- extra-passes ---- 2 | 3 | extra.p = with( 4 | list( 5 | temp.df = map_dfr( #<< 6 | 1:5000, #<< 7 | function(d) { #<< 8 | data.frame( #<< 9 | draw = d, #<< 10 | mu = model.matrix( #<< 11 | pass.m, #<< 12 | data = df %>% #<< 13 | filter(tutoring) %>% #<< 14 | mutate(tutoring = F) #<< 15 | ) %*% #<< 16 | rnorm(nrow(coefs.df), #<< 17 | mean = coefs.df$est, #<< 18 | sd = coefs.df$se) #<< 19 | ) #<< 20 | } #<< 21 | ) #<< 22 | ), 23 | { 24 | temp.df %>% 25 | mutate(pred = runif(n()) < invlogit(mu)) %>% 26 | group_by(draw) %>% 27 | summarise(pred.passed = sum(pred)) %>% 28 | ungroup() %>% 29 | mutate(extra.passed = #<< 30 | sum(df$passed & df$tutoring) #<< 31 | - pred.passed) %>% #<< 32 | ggplot(aes(x = extra.passed)) + 33 | geom_histogram(fill = "gray") + 34 | geom_vline(xintercept = 0) + 35 | labs(x = "Number of extra students\nwho passed because of tutoring", 36 | y = "Number of simulations", 37 | title = "Estimated number of extra students\nwho passed because of tutoring") 38 | } 39 | ) 40 | 41 | ## ---- extra-passes-by-group ---- 42 | 43 | extra.group.p = with( 44 | list( 45 | temp.df = map_dfr(1:5000, 46 | function(d) { data.frame(draw = d, 47 | pet.type = df$pet.type[df$tutoring], 48 | mu = model.matrix(pass.m, data = df %>% filter(tutoring) %>% mutate(tutoring = F)) %*% 49 | rnorm(nrow(coefs.df), mean = coefs.df$est, sd = coefs.df$se)) }) 50 | ), 51 | { temp.df %>% 52 | mutate(pred = runif(n()) < invlogit(mu)) %>% 53 | group_by(pet.type, draw) %>% #<< 54 | summarise(pred.passed = sum(pred), .groups = "keep") %>% 55 | ungroup() %>% 56 | left_join(df %>% #<< 57 | filter(tutoring) %>% #<< 58 | group_by(pet.type) %>% #<< 59 | summarise(actual.passed = #<< 60 | sum(passed)) %>% #<< 61 | ungroup(), #<< 62 | by = "pet.type") %>% #<< 63 | mutate(pet.type = str_to_title(pet.type), extra.passed = actual.passed - pred.passed) %>% 64 | group_by(pet.type) %>% 65 | summarise(lower.95 = quantile(extra.passed, 0.025), lower.50 = quantile(extra.passed, 0.25), median = median(extra.passed), upper.50 = quantile(extra.passed, 0.75), upper.95 = quantile(extra.passed, 0.975)) %>% 66 | ungroup() %>% 67 | mutate(pet.type = fct_reorder(pet.type, median)) %>% 68 | ggplot(aes(x = pet.type)) + 69 | geom_linerange(aes(ymin = lower.95, ymax = upper.95), size = 1) + 70 | geom_linerange(aes(ymin = lower.50, ymax = upper.50), size = 2) + 71 | geom_point(aes(y = median), size = 3) + 72 | geom_hline(yintercept = 0) + 73 | labs(subtitle = "By type of pet", x = "Pet type", 74 | y = "Number of extra students\nwho passed because of tutoring", 75 | title = "Estimated number of extra students\nwho passed because of tutoring") + 76 | coord_flip() } 77 | ) 78 | 79 | ## ---- potential-passes-by-group ---- 80 | 81 | potential.group.p = with( 82 | list( 83 | temp.df = map_dfr(1:5000, 84 | function(d) { data.frame(draw = d, 85 | pet.type = df$pet.type[!df$tutoring], 86 | mu = model.matrix(pass.m, data = df %>% filter(!tutoring) %>% mutate(tutoring = T)) %*% 87 | rnorm(nrow(coefs.df), mean = coefs.df$est, sd = coefs.df$se)) }) 88 | ), 89 | { temp.df %>% 90 | mutate(pred = runif(n()) < invlogit(mu)) %>% 91 | group_by(pet.type, draw) %>% 92 | summarise(n.passed = sum(pred), #<< 93 | .groups = "keep") %>% #<< 94 | ungroup() %>% 95 | group_by(pet.type) %>% 96 | summarise(lower.95 = quantile(n.passed, 0.025), lower.50 = quantile(n.passed, 0.25), upper.50 = quantile(n.passed, 0.75), upper.95 = quantile(n.passed, 0.975), n.passed = median(n.passed)) %>% 97 | ungroup() %>% 98 | mutate(pass.type = "Predicted") %>% #<< 99 | bind_rows( #<< 100 | df %>% #<< 101 | filter(!tutoring) %>% #<< 102 | group_by(pet.type) %>% #<< 103 | summarise(n.passed = sum(passed)) %>% #<< 104 | ungroup() %>% #<< 105 | mutate(pass.type = "Actual") #<< 106 | ) %>% #<< 107 | mutate(pet.type = str_to_title(pet.type), pet.type = fct_reorder(pet.type, n.passed, max)) %>% 108 | ggplot(aes(x = pet.type, color = pass.type, shape = pass.type)) + 109 | geom_linerange(aes(ymin = lower.95, ymax = upper.95), size = 1, show.legend = F) + geom_linerange(aes(ymin = lower.50, ymax = upper.50), size = 2, show.legend = F) + 110 | geom_point(aes(y = n.passed), size = 3) + 111 | scale_color_manual(values = c("red", "black")) + scale_shape_manual(values = c(18, 16)) + 112 | labs(x = "Pet type", color = "", shape = "", subtitle = "By type of pet", 113 | y = "Number of untutored students\npredicted to pass with tutoring", 114 | title = "Estimated number of untutored students\nwho would have passed with tutoring") + 115 | expand_limits(y = 0) + coord_flip() } 116 | ) 117 | -------------------------------------------------------------------------------- /visual_folder/log_odds.R: -------------------------------------------------------------------------------- 1 | ## ---- change-in-log-odds ---- 2 | 3 | log.odds.p = coefs.df %>% 4 | filter(parameter != "(Intercept)") %>% 5 | mutate( 6 | pretty.parameter = fct_reorder(pretty.parameter, est), 7 | lower.95 = est + (qnorm(0.025) * se), 8 | lower.50 = est + (qnorm(0.25) * se), 9 | upper.50 = est + (qnorm(0.75) * se), 10 | upper.95 = est + (qnorm(0.975) * se), 11 | signif = case_when(p > 0.05 ~ "Not significant", 12 | est > 0 ~ "Positive", 13 | est < 0 ~ "Negative"), 14 | signif = fct_relevel(signif, "Positive", 15 | "Not significant", 16 | "Negative") 17 | ) %>% 18 | ggplot(aes(x = pretty.parameter, color = signif)) + 19 | geom_linerange(aes(ymin = lower.95, 20 | ymax = upper.95), 21 | size = 1) + 22 | geom_linerange(aes(ymin = lower.50, 23 | ymax = upper.50), 24 | size = 2) + 25 | geom_point(aes(y = est), size = 3) + 26 | geom_hline(yintercept = 0) + 27 | scale_color_manual( 28 | "Relationship to\nlog odds of passing", 29 | values = c(good.color, neutral.color, bad.color) 30 | ) + 31 | labs(x = "", y = "Change in log odds", 32 | title = "Estimated relationships between\nstudent characteristics\nand log odds of passing") + 33 | coord_flip(clip = "off") 34 | 35 | # ---- change-in-log-odds-adjusted-axis ---- 36 | 37 | secret.log.odds.p = coefs.df %>% 38 | filter(parameter != "(Intercept)") %>% 39 | mutate( 40 | pretty.parameter = fct_reorder(pretty.parameter, est), 41 | lower.95 = est + (qnorm(0.025) * se), 42 | lower.50 = est + (qnorm(0.25) * se), 43 | upper.50 = est + (qnorm(0.75) * se), 44 | upper.95 = est + (qnorm(0.975) * se), 45 | signif = case_when(p > 0.05 ~ "Not significant", 46 | est > 0 ~ "Positive", 47 | est < 0 ~ "Negative"), 48 | signif = fct_relevel(signif, "Positive", 49 | "Not significant", 50 | "Negative")) %>% 51 | ggplot(aes(x = pretty.parameter, color = signif)) + 52 | geom_linerange(aes(ymin = lower.95, ymax = upper.95), size = 1) + 53 | geom_linerange(aes(ymin = lower.50, ymax = upper.50), size = 2) + 54 | geom_point(aes(y = est), size = 3) + 55 | geom_hline(yintercept = 0) + 56 | scale_y_continuous( 57 | breaks = c(-1, 0, 1), #<< 58 | labels = c("← Lower", #<< 59 | "Same", #<< 60 | "Higher →") #<< 61 | ) + 62 | scale_color_manual( 63 | "Relationship to\nlog odds of passing", 64 | values = c(good.color, neutral.color, bad.color) 65 | ) + 66 | labs(x = "", y = "Chance of passing", 67 | title = "Estimated relationships between\nstudent characteristics\nand chance of passing") + 68 | coord_flip() 69 | -------------------------------------------------------------------------------- /visual_folder/nomograms.R: -------------------------------------------------------------------------------- 1 | ## ---- create_logit_model_with_rms ---- 2 | 3 | # This the same model as pass.m, the only difference is that we are using rms's 4 | # lrm function instead of glm. 5 | library(rms) 6 | library(tidyverse) 7 | pass.m2 <- lrm(passed ~ mac + glasses + pet.type + favorite.color + cs.prior.gpa + 8 | cs.height + tutoring, 9 | data = df %>% 10 | mutate(favorite.color = case_when(favorite.color == "blue" ~ "b", 11 | favorite.color == "green" ~ "g", 12 | favorite.color == "orange" ~ "o", 13 | favorite.color == "red" ~ "r"), 14 | favorite.color = fct_infreq(favorite.color)) 15 | ) 16 | 17 | 18 | dd <- datadist(df %>% 19 | mutate(favorite.color = case_when(favorite.color == "blue" ~ "b", 20 | favorite.color == "green" ~ "g", 21 | favorite.color == "orange" ~ "o", 22 | favorite.color == "red" ~ "r"), 23 | favorite.color = fct_infreq(favorite.color))) 24 | options(datadist = 'dd') 25 | 26 | ## ---- create_nomogram_using_rms ---- 27 | 28 | nomogram <- nomogram(pass.m2, lp = F, fun = function(x) 1 / (1 + exp(-x))) 29 | plot(nomogram) 30 | 31 | ## ---- create_dataframe_for_nomogram_values ---- 32 | 33 | predictor.names <- as.vector(names(nomogram))[1:7] 34 | list.position <- as.vector(1:length(nomogram))[1:7] 35 | 36 | fun <- function(p, n){ 37 | 38 | return(data.frame(nomogram[[n]][1:3]) %>% 39 | rename("value" = p) %>% 40 | mutate(value = as.character(value), 41 | name = p, 42 | type = "coefficient") %>% 43 | dplyr::select(name, 44 | type, 45 | value, 46 | points) 47 | ) 48 | 49 | } 50 | 51 | extract.nomo.df<- map2_dfr(predictor.names, list.position, fun) 52 | 53 | extract.nomo.df %>% 54 | mutate(name = gsub("cs.", "", name)) %>% 55 | left_join(df %>% 56 | dplyr::select(id, height, prior.gpa) %>% 57 | pivot_longer(cols = c("height", "prior.gpa")) %>% 58 | group_by(name) %>% 59 | summarise(mean = mean(value), 60 | sd = sd(value)) %>% 61 | ungroup(), 62 | by = "name") %>% 63 | mutate(value = if_else(name %in% c("height", "prior.gpa"), 64 | as.character(round((as.numeric(value) * sd) + mean)), 65 | value)) %>% 66 | rbind(data.frame(nomogram[8][[1]]) %>% 67 | rename("points" = "x") %>% 68 | mutate(name = "total.points", 69 | value = as.character(points), 70 | points = seq(0, 100, length.out = 9), 71 | type = name) 72 | ) %>% 73 | rbind(data.frame(nomogram[9][[1]][1:2]) %>% 74 | rename("points" = "x", 75 | "value" = "x.real") %>% 76 | mutate(name = "probability", 77 | value = as.character(value), 78 | points = points*0.625, 79 | type = name 80 | ) 81 | ) %>% 82 | rbind(data.frame(points = seq(0, 100, by = 10))%>% 83 | mutate(name = "points", 84 | value = as.character(seq(0, 100, by = 10)), 85 | type = "points") 86 | ) %>% 87 | group_by(name) %>% 88 | mutate(max.point = max(points), 89 | min.point = min(points)) %>% 90 | ungroup() %>% 91 | mutate(type = factor(type, levels = c("probability", 92 | "total.points", 93 | "coefficient", 94 | "points"), ordered = T), 95 | pretty.parameter = case_when(name == "prior.gpa" ~ "Prior GPA", 96 | name == "height" ~ "Height", 97 | name == "pet.type" ~ "Pet type", 98 | name == "favorite.color" ~ "Favorite color", 99 | name == "tutoring" ~ "Tutoring", 100 | name == "glasses" ~ "Glasses", 101 | name == "mac" ~ "Mac", 102 | name == "total.points" ~ "Total points", 103 | name == "probability" ~ "Probability", 104 | name == "points" ~ "Points")) 105 | 106 | ## ---- create_nomogram ---- 107 | 108 | library(ggrepel) 109 | text.size = 4 110 | nomo.g <- nomogram.df %>% 111 | ggplot(aes(y= fct_reorder(pretty.parameter, as.integer(type)), 112 | label = value, group = type)) + 113 | geom_linerange(aes(xmin = min.point, xmax = max.point)) + 114 | geom_point(aes(x = points), shape = 3, size = 1.5) + 115 | geom_text_repel(aes(x = points, label = value), 116 | size = text.size, 117 | nudge_y = -0.15, 118 | data = nomogram.df %>% 119 | group_by(name) %>% 120 | filter(length(name) <5) %>% 121 | ungroup()) + 122 | geom_text(aes(x=points), nudge_y = 0.25, size = text.size, 123 | data = nomogram.df %>% 124 | group_by(name) %>% 125 | filter(length(name) >= 5) %>% 126 | ungroup()) + 127 | labs(x = "", 128 | y = "") + 129 | theme(axis.text.x = element_blank(), 130 | axis.ticks.x = element_blank()) 131 | 132 | nomo.g 133 | 134 | ## ---- nomogram_identify_points ---- 135 | 136 | nomo.g + 137 | annotate("segment", x = 80, xend = 80, y = "Prior GPA", yend = "Points", 138 | color = good.color, size = 1, 139 | arrow = arrow(length = unit(0.1, "in"), type = "closed")) + 140 | annotate("segment", 141 | x = as.numeric(nomogram.df %>% 142 | filter(value == "dog") %>% 143 | dplyr::select(points) 144 | ), 145 | xend = as.numeric(nomogram.df %>% 146 | filter(value == "dog") %>% 147 | dplyr::select(points) 148 | ), 149 | y = "Pet type", 150 | yend = "Points", 151 | color = good.color, 152 | size = 1, 153 | arrow = arrow(length = unit(0.1, "in"), type = "closed")) 154 | 155 | ## ---- nomogram_identify_probability ---- 156 | 157 | nomo.g + 158 | annotate("segment", x = 50, xend = 50, y = "Total points", yend = "Probability", 159 | color = good.color, size = 1, 160 | arrow = arrow(length = unit(0.1, "in"), type = "closed")) 161 | 162 | -------------------------------------------------------------------------------- /visual_folder/odds.R: -------------------------------------------------------------------------------- 1 | ## ---- odds-ratio-adjusted-axis ---- 2 | 3 | odds.ratio.p = coefs.df %>% 4 | filter(parameter != "(Intercept)") %>% 5 | mutate( 6 | pretty.parameter = fct_reorder(pretty.parameter, est), 7 | lower.95 = est + (qnorm(0.025) * se), 8 | lower.50 = est + (qnorm(0.25) * se), 9 | upper.50 = est + (qnorm(0.75) * se), 10 | upper.95 = est + (qnorm(0.975) * se), 11 | signif = case_when(p > 0.05 ~ "Not significant", 12 | est > 0 ~ "Positive", 13 | est < 0 ~ "Negative"), 14 | signif = fct_relevel(signif, "Positive", 15 | "Not significant", 16 | "Negative") 17 | ) %>% 18 | mutate(across(matches("est|lower|upper"), #<< 19 | ~ exp(.))) %>% #<< 20 | ggplot(aes(x = pretty.parameter, color = signif)) + 21 | geom_linerange(aes(ymin = lower.95, 22 | ymax = upper.95), 23 | size = 1) + 24 | geom_linerange(aes(ymin = lower.50, 25 | ymax = upper.50), 26 | size = 2) + 27 | geom_point(aes(y = est), size = 3) + 28 | geom_hline(yintercept = 1) + 29 | scale_y_continuous( 30 | labels = scales::percent_format() #<< 31 | ) + 32 | scale_color_manual("Relationship to\nlog odds of passing", 33 | values = c(good.color, neutral.color, bad.color)) + 34 | labs(x = "", y = "% change in odds ratio", 35 | title = "Estimated relationships between\nstudent characteristics\nand odds ratio of passing") + 36 | coord_flip() 37 | -------------------------------------------------------------------------------- /visual_folder/probability_baseline.R: -------------------------------------------------------------------------------- 1 | ## ---- probability-relative-to-some-baseline-no-arrows ---- 2 | 3 | intercept = coefs.df$est[coefs.df$parameter == "(Intercept)"] 4 | prob.baseline.p = coefs.df %>% 5 | filter(parameter != "(Intercept)") %>% 6 | mutate(pretty.parameter = fct_reorder(pretty.parameter, est), 7 | lower.95 = est + (qnorm(0.025) * se), 8 | lower.50 = est + (qnorm(0.25) * se), 9 | upper.50 = est + (qnorm(0.75) * se), 10 | upper.95 = est + (qnorm(0.975) * se), 11 | signif = case_when(p > 0.05 ~ "Not significant", 12 | est > 0 ~ "Positive", 13 | est < 0 ~ "Negative"), 14 | signif = fct_relevel(signif, "Positive", "Not significant", "Negative")) %>% 15 | mutate(across( 16 | matches("est|lower|upper"), #<< 17 | ~ invlogit(. + intercept) #<< 18 | )) %>% 19 | ggplot(aes(x = pretty.parameter, color = signif)) + 20 | geom_linerange(aes(ymin = lower.95, ymax = upper.95), size = 1) + 21 | geom_linerange(aes(ymin = lower.50, ymax = upper.50), size = 2) + 22 | geom_point(aes(y = est), size = 3) + 23 | geom_hline( 24 | yintercept = invlogit(intercept) #<< 25 | ) + 26 | scale_y_continuous( 27 | limits = c(0, 1), #<< 28 | labels = scales::percent_format() #<< 29 | ) + 30 | scale_color_manual("Relationship to\nprobability of passing", 31 | values = c(good.color, neutral.color, bad.color)) + 32 | labs(x = "", y = "Probability of passing", 33 | title = "Estimated relationships between\nstudent characteristics\nand probability of passing") + 34 | coord_flip() + 35 | theme_bw() 36 | 37 | ## ---- probability-relative-to-some-baseline-with-arrows ---- 38 | 39 | prob.baseline.arrows.p = coefs.df %>% 40 | filter(parameter != "(Intercept)") %>% 41 | mutate(pretty.parameter = fct_reorder(pretty.parameter, est), 42 | signif = case_when(p > 0.05 ~ "Not significant", 43 | est > 0 ~ "Positive", 44 | est < 0 ~ "Negative"), 45 | signif = fct_relevel(signif, "Positive", 46 | "Not significant", 47 | "Negative"), 48 | est = invlogit(est + intercept)) %>% 49 | ggplot(aes(x = invlogit(intercept), #<< 50 | xend = est, #<< 51 | y = pretty.parameter, #<< 52 | yend = pretty.parameter, #<< 53 | color = signif)) + #<< 54 | geom_segment( #<< 55 | size = 1, #<< 56 | arrow = arrow(length = unit(0.1, "in"), #<< 57 | type = "closed") #<< 58 | ) + #<< 59 | geom_vline(xintercept = invlogit(intercept)) + 60 | scale_x_continuous( 61 | limits = c(0, 1), 62 | labels = scales::percent_format() 63 | ) + 64 | scale_color_manual("Relationship to\nprobability of passing", 65 | values = c(good.color, neutral.color, bad.color)) + 66 | labs(x = "Probability of passing", y = "", 67 | title = "Estimated relationships between\nstudent characteristics\nand probability of passing") + 68 | theme_bw() 69 | -------------------------------------------------------------------------------- /visual_folder/probability_group.R: -------------------------------------------------------------------------------- 1 | ## ---- probability-relative-to-some-baseline-and-group-no-arrows ---- 2 | 3 | prob.group.p = expand.grid(pet = c("None", "Dog", "Cat", "Fish"), 4 | other.parameter = coefs.df %>% 5 | filter(!grepl("pet\\.type|Intercept", parameter)) %>% 6 | pull(parameter)) %>% 7 | mutate(pet.parameter = paste("pet.type", str_to_lower(pet), sep = "")) %>% 8 | left_join(coefs.df, by = c("pet.parameter" = "parameter")) %>% 9 | mutate(pretty.parameter = coalesce(pretty.parameter, "Pet: None"), 10 | mu = intercept + coalesce(est, 0), 11 | baseline.mu = mu) %>% 12 | dplyr::select(pet, other.parameter, mu, baseline.mu) %>% 13 | left_join(coefs.df, by = c("other.parameter" = "parameter")) %>% 14 | mutate(pretty.parameter = fct_reorder(pretty.parameter, est), 15 | mu = mu + est, 16 | lower.95 = mu + (qnorm(0.025) * se), 17 | lower.50 = mu + (qnorm(0.25) * se), 18 | upper.50 = mu + (qnorm(0.75) * se), 19 | upper.95 = mu + (qnorm(0.975) * se), 20 | signif = case_when(p > 0.05 ~ "Not significant", 21 | est > 0 ~ "Positive", 22 | est < 0 ~ "Negative"), 23 | signif = fct_relevel(signif, "Positive", "Not significant", "Negative")) %>% 24 | mutate(across(matches("mu|lower|upper"), ~ invlogit(.))) %>% 25 | ggplot(aes(x = pretty.parameter, color = signif)) + 26 | geom_linerange(aes(ymin = lower.95, ymax = upper.95), size = 1) + 27 | geom_linerange(aes(ymin = lower.50, ymax = upper.50), size = 2) + 28 | geom_point(aes(y = mu), size = 3) + 29 | geom_hline(aes(yintercept = baseline.mu)) + 30 | scale_y_continuous(limits = c(0, 1), labels = scales::percent_format()) + 31 | scale_color_manual("Relationship to\nprobability of passing", 32 | values = c(good.color, neutral.color, bad.color)) + 33 | facet_wrap(~ pet) + 34 | theme(panel.spacing.x = unit(0.65, "lines")) + 35 | labs(x = "", y = "Probability of passing", subtitle = "By type of pet", 36 | title = "Estimated relationships between\nstudent characteristics\nand probability of passing") + 37 | coord_flip() 38 | 39 | ## ---- probability-relative-to-some-baseline-and-group-with-arrows ---- 40 | 41 | prob.group.arrows.p = expand.grid(pet = c("None", "Dog", "Cat", "Fish"), 42 | other.parameter = coefs.df %>% 43 | filter(!grepl("pet\\.type|Intercept", parameter)) %>% 44 | pull(parameter)) %>% 45 | mutate(pet.parameter = paste("pet.type", str_to_lower(pet), sep = "")) %>% 46 | left_join(coefs.df, by = c("pet.parameter" = "parameter")) %>% 47 | mutate(pretty.parameter = coalesce(pretty.parameter, "Pet: None"), 48 | mu = coefs.df$est[coefs.df$parameter == "(Intercept)"] + 49 | coalesce(est, 0), 50 | baseline.mu = mu) %>% 51 | dplyr::select(pet, other.parameter, mu, baseline.mu) %>% 52 | left_join(coefs.df, by = c("other.parameter" = "parameter")) %>% 53 | mutate(pretty.parameter = fct_reorder(pretty.parameter, est), 54 | mu = mu + est, 55 | signif = case_when(p > 0.05 ~ "Not significant", 56 | est > 0 ~ "Positive", 57 | est < 0 ~ "Negative"), 58 | signif = fct_relevel(signif, "Positive", "Not significant", 59 | "Negative")) %>% 60 | mutate(across(matches("mu"), ~ invlogit(.))) %>% 61 | ggplot(aes(x = baseline.mu, xend = mu, y = pretty.parameter, 62 | yend = pretty.parameter, color = signif)) + 63 | geom_segment(size = 1, 64 | arrow = arrow(length = unit(0.1, "in"), type = "closed")) + 65 | geom_vline(aes(xintercept = baseline.mu)) + 66 | scale_x_continuous(limits = c(0, 1), labels = scales::percent_format()) + 67 | scale_color_manual("Relationship to\nprobability of passing", 68 | values = c(good.color, neutral.color, bad.color)) + 69 | facet_wrap(~ pet) + 70 | theme(panel.spacing.x = unit(0.65, "lines")) + 71 | labs(x = "Probability of passing", y = "", 72 | title = "Estimated relationships between\nstudent characteristics\nand probability of passing", 73 | subtitle = "By type of pet") 74 | -------------------------------------------------------------------------------- /xaringan-themer.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------- 2 | * 3 | * !! This file was generated by xaringanthemer !! 4 | * 5 | * Changes made to this file directly will be overwritten 6 | * if you used xaringanthemer in your xaringan slides Rmd 7 | * 8 | * Issues or likes? 9 | * - https://github.com/gadenbuie/xaringanthemer 10 | * - https://www.garrickadenbuie.com 11 | * 12 | * Need help? Try: 13 | * - vignette(package = "xaringanthemer") 14 | * - ?xaringanthemer::style_xaringan 15 | * - xaringan wiki: https://github.com/yihui/xaringan/wiki 16 | * - remarkjs wiki: https://github.com/gnab/remark/wiki 17 | * 18 | * Version: 0.4.2 19 | * 20 | * ------------------------------------------------------- */ 21 | @import url(https://fonts.googleapis.com/css?family=Noto+Sans:400,400i,700,700i&display=swap); 22 | @import url(https://fonts.googleapis.com/css?family=Cabin:600,600i&display=swap); 23 | @import url(https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700&display=swap); 24 | 25 | 26 | :root { 27 | /* Fonts */ 28 | --text-font-family: 'Noto Sans'; 29 | --text-font-is-google: 1; 30 | --text-font-family-fallback: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui, arial; 31 | --text-font-base: sans-serif; 32 | --header-font-family: Cabin; 33 | --header-font-is-google: 1; 34 | --header-font-family-fallback: Georgia, serif; 35 | --code-font-family: 'Source Code Pro'; 36 | --code-font-is-google: 1; 37 | --base-font-size: 20px; 38 | --text-font-size: 1rem; 39 | --code-font-size: 0.9rem; 40 | --code-inline-font-size: 1em; 41 | --header-h1-font-size: 2.75rem; 42 | --header-h2-font-size: 2.25rem; 43 | --header-h3-font-size: 1.75rem; 44 | 45 | /* Colors */ 46 | --text-color: #000000; 47 | --header-color: #003865; 48 | --background-color: #FFFFFF; 49 | --link-color: #003865; 50 | --text-bold-color: #003865; 51 | --code-highlight-color: rgba(255,255,0,0.5); 52 | --inverse-text-color: #000000; 53 | --inverse-background-color: #FFCD00; 54 | --inverse-header-color: #000000; 55 | --inverse-link-color: #003865; 56 | --title-slide-background-color: #003865; 57 | --title-slide-text-color: #FFFFFF; 58 | --header-background-color: #003865; 59 | --header-background-text-color: #FFFFFF; 60 | --primary: #003865; 61 | --secondary: #FFCD00; 62 | --white: #FFFFFF; 63 | --black: #000000; 64 | } 65 | 66 | html { 67 | font-size: var(--base-font-size); 68 | } 69 | 70 | body { 71 | font-family: var(--text-font-family), var(--text-font-family-fallback), var(--text-font-base); 72 | font-weight: normal; 73 | color: var(--text-color); 74 | } 75 | h1, h2, h3 { 76 | font-family: var(--header-font-family), var(--header-font-family-fallback); 77 | font-weight: 600; 78 | color: var(--header-color); 79 | } 80 | .remark-slide-content { 81 | background-color: var(--background-color); 82 | font-size: 1rem; 83 | padding: 16px 64px 16px 64px; 84 | width: 100%; 85 | height: 100%; 86 | } 87 | .remark-slide-content h1 { 88 | font-size: var(--header-h1-font-size); 89 | } 90 | .remark-slide-content h2 { 91 | font-size: var(--header-h2-font-size); 92 | } 93 | .remark-slide-content h3 { 94 | font-size: var(--header-h3-font-size); 95 | } 96 | .remark-code, .remark-inline-code { 97 | font-family: var(--code-font-family), Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; 98 | } 99 | .remark-code { 100 | font-size: var(--code-font-size); 101 | } 102 | .remark-inline-code { 103 | font-size: var(--code-inline-font-size); 104 | color: #003865; 105 | } 106 | .remark-slide-number { 107 | color: #003865; 108 | opacity: 1; 109 | font-size: 0.9rem; 110 | } 111 | strong { 112 | font-weight: bold; 113 | color: var(--text-bold-color); 114 | } 115 | a, a > code { 116 | color: var(--link-color); 117 | text-decoration: none; 118 | } 119 | .footnote { 120 | position: absolute; 121 | bottom: 60px; 122 | padding-right: 4em; 123 | font-size: 0.9em; 124 | } 125 | .remark-code-line-highlighted { 126 | background-color: var(--code-highlight-color); 127 | } 128 | .inverse { 129 | background-color: var(--inverse-background-color); 130 | color: var(--inverse-text-color); 131 | 132 | } 133 | .inverse h1, .inverse h2, .inverse h3 { 134 | color: var(--inverse-header-color); 135 | } 136 | .inverse a, .inverse a > code { 137 | color: var(--inverse-link-color); 138 | } 139 | .title-slide, .title-slide h1, .title-slide h2, .title-slide h3 { 140 | color: var(--title-slide-text-color); 141 | } 142 | .title-slide { 143 | background-color: var(--title-slide-background-color); 144 | } 145 | .title-slide .remark-slide-number { 146 | display: none; 147 | } 148 | /* Two-column layout */ 149 | .left-column { 150 | width: 20%; 151 | height: 92%; 152 | float: left; 153 | } 154 | .left-column h2, .left-column h3 { 155 | color: #00386599; 156 | } 157 | .left-column h2:last-of-type, .left-column h3:last-child { 158 | color: #003865; 159 | } 160 | .right-column { 161 | width: 75%; 162 | float: right; 163 | padding-top: 1em; 164 | } 165 | .pull-left { 166 | float: left; 167 | width: 47%; 168 | } 169 | .pull-right { 170 | float: right; 171 | width: 47%; 172 | } 173 | .pull-right + * { 174 | clear: both; 175 | } 176 | img, video, iframe { 177 | max-width: 100%; 178 | } 179 | blockquote { 180 | border-left: solid 5px #FFCD0080; 181 | padding-left: 1em; 182 | } 183 | .remark-slide table { 184 | margin: auto; 185 | border-top: 1px solid #666; 186 | border-bottom: 1px solid #666; 187 | } 188 | .remark-slide table thead th { 189 | border-bottom: 1px solid #ddd; 190 | } 191 | th, td { 192 | padding: 5px; 193 | } 194 | .remark-slide table:not(.table-unshaded) thead, 195 | .remark-slide table:not(.table-unshaded) tfoot, 196 | .remark-slide table:not(.table-unshaded) tr:nth-child(even) { 197 | background: #FFF5CC; 198 | } 199 | table.dataTable tbody { 200 | background-color: var(--background-color); 201 | color: var(--text-color); 202 | } 203 | table.dataTable.display tbody tr.odd { 204 | background-color: var(--background-color); 205 | } 206 | table.dataTable.display tbody tr.even { 207 | background-color: #FFF5CC; 208 | } 209 | table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { 210 | background-color: rgba(255, 255, 255, 0.5); 211 | } 212 | .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate { 213 | color: var(--text-color); 214 | } 215 | .dataTables_wrapper .dataTables_paginate .paginate_button { 216 | color: var(--text-color) !important; 217 | } 218 | 219 | /* Horizontal alignment of code blocks */ 220 | .remark-slide-content.left pre, 221 | .remark-slide-content.center pre, 222 | .remark-slide-content.right pre { 223 | text-align: start; 224 | width: max-content; 225 | max-width: 100%; 226 | } 227 | .remark-slide-content.left pre, 228 | .remark-slide-content.right pre { 229 | min-width: 50%; 230 | min-width: min(40ch, 100%); 231 | } 232 | .remark-slide-content.center pre { 233 | min-width: 66%; 234 | min-width: min(50ch, 100%); 235 | } 236 | .remark-slide-content.left pre { 237 | margin-left: unset; 238 | margin-right: auto; 239 | } 240 | .remark-slide-content.center pre { 241 | margin-left: auto; 242 | margin-right: auto; 243 | } 244 | .remark-slide-content.right pre { 245 | margin-left: auto; 246 | margin-right: unset; 247 | } 248 | 249 | /* Slide Header Background for h1 elements */ 250 | .remark-slide-content.header_background > h1 { 251 | display: block; 252 | position: absolute; 253 | top: 0; 254 | left: 0; 255 | width: 100%; 256 | background: var(--header-background-color); 257 | color: var(--header-background-text-color); 258 | padding: 2rem 64px 1.5rem 64px; 259 | margin-top: 0; 260 | box-sizing: border-box; 261 | } 262 | .remark-slide-content.header_background { 263 | padding-top: 7rem; 264 | } 265 | 266 | @page { margin: 0; } 267 | @media print { 268 | .remark-slide-scaler { 269 | width: 100% !important; 270 | height: 100% !important; 271 | transform: scale(1) !important; 272 | top: 0 !important; 273 | left: 0 !important; 274 | } 275 | } 276 | 277 | .primary { 278 | color: var(--primary); 279 | } 280 | .bg-primary { 281 | background-color: var(--primary); 282 | } 283 | .secondary { 284 | color: var(--secondary); 285 | } 286 | .bg-secondary { 287 | background-color: var(--secondary); 288 | } 289 | .white { 290 | color: var(--white); 291 | } 292 | .bg-white { 293 | background-color: var(--white); 294 | } 295 | .black { 296 | color: var(--black); 297 | } 298 | .bg-black { 299 | background-color: var(--black); 300 | } 301 | 302 | --------------------------------------------------------------------------------