blogposts

Code used to generate some blog posts.
git clone https://git.eamoncaddigan.net/blogposts.git
Log | Files | Refs | README | LICENSE

2018-07-22-tufte_plot.Rmd (11834B)


      1 ---
      2 title: "Recreating a Tufte Slopegraph"
      3 author: "Eamon Caddigan"
      4 date: "2018-07-05"
      5 output: md_document
      6 ---
      7 
      8 ```{r setup, include=FALSE}
      9 knitr::opts_chunk$set(echo = TRUE)
     10 ```
     11 ```{r load_libraries, include=FALSE, warning=FALSE, message=FALSE}
     12 #extrafont::loadfonts(device="win")
     13 suppressPackageStartupMessages(library(tibble))
     14 suppressPackageStartupMessages(library(dplyr))
     15 suppressPackageStartupMessages(library(ggplot2))
     16 suppressPackageStartupMessages(library(ggrepel))
     17 ```
     18 
     19 Recently on Twitter, data visualization guru Edward R. Tufte wrote that graphics produced by R are not “publication ready”. His proposed workflow is to use statistical software to create an initial version of a plot, and then make final improvements in Adobe Illustrator.
     20 
     21 I disagree with this advice. First I’ll show the steps a data analyst might take to create a high-quality graphic entirely in R. Then, I’ll explain why I think this is a better approach.
     22 
     23 ## Publication quality graphics in R
     24 
     25 Page 158 of Tufte’s classic book, [The Visual Display of Quantitative Information (2nd ed.)](https://www.edwardtufte.com/tufte/books_vdqi), features a “slope graph” that shows the change in government receipts for several countries between 1970 and 1979. Below are the first few rows of these data in a [tidy data frame](https://www.jstatsoft.org/article/view/v059i10), `receiptData`, along with a quick and dirty slopegraph.
     26 
     27 ```{r receipt_data, echo=FALSE}
     28 receiptData <- tribble(
     29                          ~country, ~year, ~receipts,
     30                         "Belgium", 1970L,      35.2,
     31                         "Belgium", 1979L,      43.2,
     32                         "Britain", 1970L,      40.7,
     33                         "Britain", 1979L,      39.0,
     34                          "Canada", 1970L,      35.2,
     35                          "Canada", 1979L,      35.8,
     36                         "Finland", 1970L,      34.9,
     37                         "Finland", 1979L,      38.2,
     38                          "France", 1970L,      39.0,
     39                          "France", 1979L,      43.4,
     40                         "Germany", 1970L,      37.5,
     41                         "Germany", 1979L,      42.9,
     42                          "Greece", 1970L,      26.8,
     43                          "Greece", 1979L,      30.6,
     44                           "Italy", 1970L,      30.4,
     45                           "Italy", 1979L,      35.7,
     46                           "Japan", 1970L,      20.7,
     47                           "Japan", 1979L,      26.6,
     48                     "Netherlands", 1970L,      44.0,
     49                     "Netherlands", 1979L,      55.8,
     50                          "Norway", 1970L,      43.5,
     51                          "Norway", 1979L,      52.2,
     52                           "Spain", 1970L,      22.5,
     53                           "Spain", 1979L,      27.1,
     54                          "Sweden", 1970L,      46.9,
     55                          "Sweden", 1979L,      57.4,
     56                     "Switzerland", 1970L,      26.5,
     57                     "Switzerland", 1979L,      33.2,
     58                   "United States", 1970L,      30.3,
     59                   "United States", 1979L,      32.5
     60                  )
     61 
     62 receiptData %>%
     63   head(6) %>%
     64   knitr::kable()
     65 ```
     66 ```{r graph_1}
     67 ggplot(receiptData, aes(year, receipts, group = country)) +
     68   geom_line() +
     69   geom_text_repel(aes(label = country)) +
     70   labs(x = "Year", y = "Government receipts as percentage of GDP")
     71 ```
     72 
     73 This plot is not attractive, but it is useful for getting a handle on the data. Whether [iterating through an exploratory data analysis]({{< ref "/posts/2015-09-09-data-science.md" >}}) or preparing a graphic for publication, analysts will create many ugly graphics on the path to settling on a design and refining it.
     74 
     75 For our first round of improvements, we can change the aspect ratio of the graphic and arrange the country labels so they don’t overlap with the data. We should also remove the “chart junk” in the background, such as the background grid, and label only the years of interest on the x-axis.
     76 
     77 ```{r graph_2, fig.height=8, fig.width=5}
     78 ggplot(receiptData, aes(year, receipts, group = country)) +
     79   geom_line() +
     80   geom_text_repel(aes(label = country),
     81                   data = filter(receiptData, year == 1970),
     82                   nudge_x = -0.5, hjust = 1,
     83                   direction = "y", size = 5) +
     84   geom_text_repel(aes(label = country),
     85                   data = filter(receiptData, year == 1979),
     86                   nudge_x = 0.5, hjust = 0, 
     87                   direction = "y", size = 5) +
     88   scale_x_continuous(breaks = c(1970, 1979), limits = c(1966, 1983)) +
     89   theme(panel.background = element_blank(),
     90         axis.title = element_text(size = 16),
     91         axis.text = element_text(size = 12)) +
     92   labs(x = "Year", y = "Government receipts as percentage of GDP")
     93 ```
     94 
     95 The [ggrepel](https://github.com/slowkow/ggrepel) package has done a great job preventing the labels from overlapping; I usually use `geom_text_repel()` instead of `geom_text()` during exploratory data analysis. Here, however, the segments connecting the labels to data points create confusing clutter. While these can be removed within the function, our final figure will be easier to understand if we nudge labels manually so that they’re as close to their data as possible.
     96 
     97 We can also drop the axes; the year labels will go right above the data, and we’ll show each country’s value next to its label. Since we’re losing the axis titles, we’ll also embed a caption in this version of the graphic.
     98 
     99 Finally, we’ll change the typeface. Since we’re trying to make something that would please Tufte, we’ll use a serif font. If you haven’t done so before, you may need to run `loadfonts()` from the [extrafont package](https://github.com/wch/extrafont) to tell R how to find system fonts. We’ll also further boost our “data-ink ratio” by making the lines thinner.
    100 
    101 ```{r graph_3, fig.height=8, fig.width=8}
    102 labelAdjustments <- tribble(
    103                               ~country, ~year, ~nudge_y,
    104                          "Netherlands", 1970L,      0.3,
    105                               "Norway", 1970L,     -0.2,
    106                              "Belgium", 1970L,      0.9,
    107                               "Canada", 1970L,     -0.1,
    108                              "Finland", 1970L,     -0.8,
    109                                "Italy", 1970L,      0.4,
    110                        "United States", 1970L,     -0.5,
    111                               "Greece", 1970L,      0.4,
    112                          "Switzerland", 1970L,     -0.3,
    113                               "France", 1979L,      0.8,
    114                              "Germany", 1979L,     -0.7,
    115                              "Britain", 1979L,      0.1,
    116                              "Finland", 1979L,     -0.1,
    117                               "Canada", 1979L,      0.4,
    118                                "Italy", 1979L,     -0.5,
    119                          "Switzerland", 1979L,      0.2,
    120                        "United States", 1979L,     -0.1,
    121                                "Spain", 1979L,      0.3,
    122                                "Japan", 1979L,     -0.2
    123                       )
    124 
    125 receiptData <- left_join(receiptData, labelAdjustments,
    126                            by = c("country", "year")) %>% 
    127   mutate(receipts_nudged = ifelse(is.na(nudge_y), 0, nudge_y) + receipts)
    128 
    129 update_geom_defaults("text", list(family = "Georgia", size = 4.0))
    130 ggplot(receiptData, aes(year, receipts, group = country)) +
    131   # Slope lines
    132   geom_line(size = 0.1) +
    133   # Country names (manually nudged)
    134   geom_text(aes(year, receipts_nudged, label = country),
    135             data = filter(receiptData, year == 1970),
    136             hjust = 1, nudge_x = -3) +
    137   geom_text(aes(year, receipts_nudged, label = country),
    138             data = filter(receiptData, year == 1979),
    139             hjust = 0, nudge_x = 3) +
    140   # Data values
    141   geom_text(aes(year, receipts_nudged, label = sprintf("%0.1f", receipts)),
    142             data = filter(receiptData, year == 1970),
    143             hjust = 1, nudge_x = -0.5) +
    144   geom_text(aes(year, receipts_nudged, label = sprintf("%0.1f", receipts)),
    145             data = filter(receiptData, year == 1979),
    146             hjust = 0, nudge_x = 0.5) +
    147   # Column labels
    148   annotate("text", x = c(1970, 1979) + c(-0.5, 0.5), 
    149            y = max(receiptData$receipts) + 3,
    150            label = c("1970", "1979"), 
    151            hjust = c(1, 0)) +
    152   # Plot annotation
    153   annotate("text", x = 1945, y = 58, hjust = 0, vjust = 1,
    154            label = paste("Current Receipts of Government as a", 
    155                          "Percentage of Gross Domestic\nProduct, 1970 and 1979",
    156                          sep = "\n"),
    157            family = "Georgia", size = 4.0) +
    158   coord_cartesian(xlim = c(1945, 1990)) +
    159   theme(panel.background = element_blank(),
    160         axis.title = element_blank(),
    161         axis.text = element_blank(),
    162         axis.ticks = element_blank())
    163 ```
    164 
    165 ## Why it’s worth it
    166 
    167 It took several iterations to get the aesthetics of this plot just right, while an adept user of a graphics editor would be able to recreate it in minutes. However, this initial time savings obscures several advantages to completing this process in code as opposed to switching to a tool like Illustrator.
    168 
    169 ### R is free software
    170 
    171 All of the programs I used to create this graphic (and share it with you) are [free software](https://www.fsf.org/about/what-is-free-software), unlike Adobe’s terrific but expensive Illustrator. [Inkscape](https://inkscape.org/en/) is an excellent open source alternative, but it is not as powerful as its commercial competitor. On the other hand, [R](https://www.r-project.org/) is arguably the most advanced and well-supported statistical analysis tool. Even if the politics of the open source movement aren’t important, free is a better price-point.
    172 
    173 ### Code can be reproduced
    174 
    175 Anybody who wants to recreate the final figure using a different set of countries, a new pair of years, or a favored economic indicator can run the code above on their own data. With minimal tweaking, they will have a graphic with aesthetics identical to those above. Reproducing this graphic using a multi-tool workflow is more complicated; a novice would first need to learn how to create a “rough draft” of it in software, and then learn how to use a graphics editor to improve its appearance.
    176 
    177 ### Scripted graphics are easier to update
    178 
    179 Imagine that an analyst discovered a mistake in their analysis that needed to be corrected. Or imagine the less stressful situation where a colleague suggested replacing some of the countries in the figure. In such cases, the work of creating the graphic will need to be redone: the analyst will need to add data or correct errors, generate a new plot in software, and re-edit the plot in their graphics editor. Using a scripted, code-based workflow, the analyst would only need to do the first step, and possibly update some manual tweaks. The initial time savings afforded by a graphics editor disappears after the first time this happens.
    180 
    181 ### Automation prevents errors
    182 
    183 Not only does scripted workflow make it easier to deal with errors, it guards against them. When analysts adjust graphic elements in a GUI, there’s a risk of mistakenly moving, deleting, or mislabeling data points. Such mistakes are difficult to detect, but code-based workflows avoid these risks; if the data source and analysis are error-free, then the graphic will also be.
    184 
    185 These considerations are among the reasons why scripted analyses are [considered best practice](https://peerj.com/preprints/3210/) in statistical analysis. Most of Tufte’s advice on creating graphics is excellent, but I recommend ignoring his suggestion to make the final edits in a program like Illustrator. Learning how to make polished graphics in software will ultimately save analysts time and help them avoid mistakes.