Summarising tables

Approach to streamline workflow when summarising tables

Introduction

The result of the data science process is to communicate findings, typically to an audience that doesn’t talk technical. It is the most important deliverable of the process, even if not the first thing that springs to mind when considering data science. Fantastic insights are of no use if the intended audience doesn’t understand or trust it. It is therefore vital to take care when presenting findings.

There are typical and often repeated actions when summarising data in tables. It adds overhead to transform analysis specifically to create presentation-ready results. I have developed an approach and associated functions that streamline these tasks, with the benefit of making presentation consistent.

The following scenarios are addressed:

Scenarios when summarising tables

Scenarios when summarising tables

  • Boolean Features
    I often create and use boolean features as it makes analysis consistent and easier to follow, especially when used as filter conditions.

    However, it is difficult to review summary tables when labels read TRUE or FALSE, especially when multiple boolean features are presented. Replacing it with descriptive labels improves legibility.
    See Credit Card example shown above.

    The function func_legible_boolean in the code block below achieves this.

  • Table Summaries
    By row or column, either summarising mulitple columns into one, or the content of each selected column. Summaries are often a sum total of numeric fields, but can be other summary statistics too.

    The function func_create_summary creates a row summary.

  • Headers
    Remember the days when you had to dial a phone number when making a call? 😄

    It is a lot easier to code when features have no spaces, specifically using the autocomplete feature of code editors. I therefore use underscores as placeholders.

    It is a good idea though to revert the underscores to spaces when presenting tables. func_present_headers takes care of this.

Code

Library

Load the packages used in the demonstration.

package_list <- c("tidyverse", "lubridate", "knitr")
invisible(suppressPackageStartupMessages(lapply(package_list, library, character.only = TRUE)))
rm(package_list)
options(digits=9)

Functions

The functions instantiated in following code block addresses the scenarios listed above. The inline comments below describe the working and sequence of each function.

func_rename <-
  function(x) {
    
    # sequence is important here
    # regex removing all chars not alpha-numeric, underscore or period
    gsub("[^[:alnum:] \\_\\.]", "", x) %>%
      
      # replace any space or period with underscore
      str_replace_all(pattern = " |\\.", replacement = "\\_") %>%
      
      # replace multiple underscores with one
      str_replace_all(pattern = "\\_+", replacement = "\\_") %>%
      
      # remove trailing underscores and period
      str_remove(pattern = "\\_$|\\.$")
  }

func_legible_boolean <-
  
  # returns `feature name` when TRUE and "Not_" + `feature name` otherwise
  function(df) {
    
    # add row numbers to input dataframe
    df <-
      df %>%
      ungroup() %>%
      mutate(row_id = row_number())
    
    # select row_id and other logical fields
    tmp <-
      df %>%
      select(row_id, select_if(., is.logical) %>% names(.)) %>%
      
      # gather all logical fields and keep row_id to retain identity
      gather(key, value,-row_id) %>%
      
      # when value is TRUE then return the feature name else "Not_" pasted in
      # front of it
      mutate(value = case_when(
        value ~ key,
        TRUE ~ paste("Not", key, sep = "_")
      )) %>%
      
      # return the feature names back to header positions
      spread(key, value)
    
    vars <- 
      df %>%
      select_if(is.logical) %>%
      names()
    
    df %>% 
      
      # return all values from the table after excluding the original logical
      # fields and join the newly adjuted features back into the table using the
      # row_id
      select(-(!!vars)) %>%
      inner_join(tmp,
                 by = "row_id") %>%
      
      # remove the row_id as the join is complete
      select(-row_id)
  }

func_create_summary <-
  
  # group by factors, summarising numeric fields to sum total
  function(df) {
    
    # create a list of factors from the input dataframe
    tmp_factors <- df %>% select_if(is.factor) %>% names()
    
    # mutate all factors in the dataframe to characters
    df <- df %>% ungroup() %>% mutate_if(is.factor, as.character)
    
    
    # list original dataframe with a newly created summary row
    list(
      
      # original input dataframe
      df,
      
      # summary row, replaceing each factor value to "Total"
      df %>%
        mutate_at(tmp_factors, ~"Total") %>%
        
        # group by for each of the original factor features
        group_by_at(tmp_factors) %>%
        
        # summarise all numeric values to sum total, ignoring NULL values
        summarise_if(is.numeric, sum, na.rm = TRUE)
    ) %>% 
      
      # union all tables contained in the list, including original dataframe and
      # newly created summary row
      reduce(union_all) %>%
      
      # change factors back into factors
      mutate_at(tmp_factors, factor) %>% 
      return()
  }


func_present_headers <-
  function(.) {
    
    # replace all `spaceholder underscores` with spaces
    str_replace_all(., pattern = "_", replacement = " ") %>%
      
      # change all words in string to title text
      str_to_title() %>%
      return()
  }

Parameters and Configuration

I am using the Brazilian E-Commerce Public Dataset by Olist from kaggle Datasets in this demonstration. The var_path parameter stores the location of the source data within my environment.

var_path <- "~/Documents/Training/datasets/brazilian-ecommerce/"

Import

Please note that, parsing in this case, the messages upon readr::read_csv are disabled to make the article more legible.

The code sequence below is written to import a nominated subset of files into a single dataframe (datablock), nesting each file as a tibble.

df_files <-
  
  # traverse the specified directory and add list of files to a table and rename
  # the tibble's default column value to file_name_ext, representing the name of
  # the file and its extention
  list.files(path = var_path,
             pattern = ".",
             recursive = TRUE) %>%
  as_tibble() %>%
  rename(file_name_ext = value) %>%
  
  # remove the file extention to isolate the file name as a separate variable
  mutate_at("file_name_ext", funs(file_name = str_remove), pattern = "\\.csv") %>% 
  
  # build a file path input for each file as used by the file parameter in the
  # read_csv function
  mutate(file_path = paste0(var_path, file_name_ext)) %>%
  
  # map the file_path to readr::read_csv to iteratively upload each file listed in the
  # table
  mutate(file = map(file_path, function(param_file_path) {
    read_csv(file = param_file_path) %>%
      return()
  })) %>%
  
  # standardise the naming of each file in situ
  mutate_at("file", map, function(df) {
    df %>% 
      rename_all(func_rename) %>% 
      return()
  }) %>% 
  
  # remove redundant variables to tidy up the datablock
  select(-file_path, -file_name_ext)

# print new table to glance the imported product
df_files
## # A tibble: 9 x 2
##   file_name                         file                    
##   <chr>                             <list>                  
## 1 olist_customers_dataset           <tibble [99,441 × 5]>   
## 2 olist_geolocation_dataset         <tibble [1,000,163 × 5]>
## 3 olist_order_items_dataset         <tibble [112,650 × 6]>  
## 4 olist_order_payments_dataset      <tibble [103,886 × 5]>  
## 5 olist_order_reviews_dataset       <tibble [100,000 × 7]>  
## 6 olist_orders_dataset              <tibble [99,441 × 8]>   
## 7 olist_products_dataset            <tibble [32,951 × 9]>   
## 8 olist_sellers_dataset             <tibble [3,095 × 4]>    
## 9 product_category_name_translation <tibble [71 × 2]>

This confirms 9 files imported, specifying the rows nrow and columns ncol of each nested tibble.

Inspect Data

The function func_inspect_file helps to extract and print the structure of nested tibbles, including olist_order_payments_dataset, olist_orders_dataset and olist_customers_dataset contained within df_files.

Changing the character fields to factors better structures and provides additional information about the features.

func_inspect_file <-
  function(param_file_name) {
    
    # create *** separators to pad tibble names followed by printed structure
    rep("*", 30) %>% paste(collapse = "") %>% print()
    print(param_file_name)
    rep("*", 30) %>% paste(collapse = "") %>% print()
    
    df_files %>%
      filter(file_name == param_file_name) %>%
      select(-file_name) %>%
      unnest() %>%
      mutate_if(is.character, factor) %>%
      head() %>%
      str() %>%
      print()
  }

tibble(
  
  # list tibble names to inspect
  param_file = c(
    "olist_order_payments_dataset",
    "olist_orders_dataset",
    "olist_customers_dataset"
  )
) %>%
  mutate_at("param_file", walk, func_inspect_file)
## [1] "******************************"
## [1] "olist_order_payments_dataset"
## [1] "******************************"
## Classes 'tbl_df', 'tbl' and 'data.frame':    6 obs. of  5 variables:
##  $ order_id            : Factor w/ 99440 levels "00010242fe8c5a6d1ba2dd792cb16214",..: 71446 65633 14657 72396 25967 16046
##  $ payment_sequential  : int  1 1 1 1 1 1
##  $ payment_type        : Factor w/ 5 levels "boleto","credit_card",..: 2 2 2 2 2 2
##  $ payment_installments: int  8 1 1 8 2 2
##  $ payment_value       : num  99.3 24.4 65.7 107.8 128.4 ...
## NULL
## [1] "******************************"
## [1] "olist_orders_dataset"
## [1] "******************************"
## Classes 'tbl_df', 'tbl' and 'data.frame':    6 obs. of  8 variables:
##  $ order_id                     : Factor w/ 99441 levels "00010242fe8c5a6d1ba2dd792cb16214",..: 88951 32546 27770 57386 67044 63543
##  $ customer_id                  : Factor w/ 99441 levels "00012a2ce6f8dcda20d059ce98491703",..: 61761 68730 25514 96584 53774 31118
##  $ order_status                 : Factor w/ 8 levels "approved","canceled",..: 4 4 4 4 4 4
##  $ order_purchase_timestamp     : POSIXct, format: "2017-10-02 10:56:33" "2018-07-24 20:41:37" ...
##  $ order_approved_at            : POSIXct, format: "2017-10-02 11:07:15" "2018-07-26 03:24:27" ...
##  $ order_delivered_carrier_date : POSIXct, format: "2017-10-04 19:55:00" "2018-07-26 14:31:00" ...
##  $ order_delivered_customer_date: POSIXct, format: "2017-10-10 21:25:13" "2018-08-07 15:27:45" ...
##  $ order_estimated_delivery_date: POSIXct, format: "2017-10-18" "2018-08-13" ...
## NULL
## [1] "******************************"
## [1] "olist_customers_dataset"
## [1] "******************************"
## Classes 'tbl_df', 'tbl' and 'data.frame':    6 obs. of  5 variables:
##  $ customer_id             : Factor w/ 99441 levels "00012a2ce6f8dcda20d059ce98491703",..: 2611 9562 30461 69606 30708 52562
##  $ customer_unique_id      : Factor w/ 96096 levels "0000366f3b9a7992bf8c76cfdf3221e2",..: 50397 15434 2273 14193 19734 28806
##  $ customer_zip_code_prefix: Factor w/ 14994 levels "01003","01004",..: 4774 3802 68 3586 4307 13965
##  $ customer_city           : Factor w/ 4119 levels "abadia dos dourados",..: 1383 3429 3598 2342 708 1937
##  $ customer_state          : Factor w/ 27 levels "AC","AL","AM",..: 26 26 26 26 26 24
## NULL
## # A tibble: 3 x 1
##   param_file                  
##   <chr>                       
## 1 olist_order_payments_dataset
## 2 olist_orders_dataset        
## 3 olist_customers_dataset

Join

Iterating through a list of tibbles with matching primary and foreign keys will result in a joined dataframe.

Primary and foreign keys on each of the tibbles results in an automatic join for each iteration, using a feature of the dplyr::full_join function to guess joins (column names shared in both tibbles) if none are explicitly provided.

Let’s run the sequence and inspect the head of the resulting datafame.

df_customer_order_payment <-
  
  # a list of tibbles to join
  tibble(
  param_file = c(
    "olist_order_payments_dataset",
    "olist_orders_dataset",
    "olist_customers_dataset"
  )
) %>%
  
  # unnest each tibble listed above, joining it with the result of the previous
  # mapping sequence
  mutate_at("param_file", map, function(param_file) {
    df_files %>%
      filter(file_name == param_file) %>%
      select(-"file_name") %>%
      unnest()
  }) %>%
  reduce(unnest) %>%
  reduce(full_join)
## Joining, by = "order_id"
## Joining, by = "customer_id"
# return head of the joined tibbles
df_customer_order_payment %>% 
  head() %>% 
  
  # specify styling options
  kable(format = "html") %>% 
  kableExtra::kable_styling(full_width = FALSE, font_size = 11) %>% 
  kableExtra::scroll_box(width = "100%", height = "200px")
order_id payment_sequential payment_type payment_installments payment_value customer_id order_status order_purchase_timestamp order_approved_at order_delivered_carrier_date order_delivered_customer_date order_estimated_delivery_date customer_unique_id customer_zip_code_prefix customer_city customer_state
b81ef226f3fe1789b1e8b2acac839d17 1 credit_card 8 99.33 0a8556ac6be836b46b3e89920d59291c delivered 2018-04-25 22:01:49 2018-04-25 22:15:09 2018-05-02 15:20:00 2018-05-09 17:36:51 2018-05-22 708ab75d2a007f0564aedd11139c7708 39801 teofilo otoni MG
a9810da82917af2d9aefd1278f1dcfa0 1 credit_card 1 24.39 f2c7fc58a9de810828715166c672f10a delivered 2018-06-26 11:01:38 2018-06-26 11:18:58 2018-06-28 14:18:00 2018-06-29 20:32:09 2018-07-16 a8b9d3a27068454b1c98cc67d4e31e6f 02422 sao paulo SP
25e8ea4e93396b6fa0d3dd708e76c1bd 1 credit_card 1 65.71 25b14b69de0b6e184ae6fe2755e478f9 delivered 2017-12-12 11:19:55 2017-12-14 09:52:34 2017-12-15 20:13:22 2017-12-18 17:24:41 2018-01-04 6f70c0b2f7552832ba46eb57b1c5651e 02652 sao paulo SP
ba78997921bbcdc1373bb41e913ab953 1 credit_card 8 107.78 7a5d8efaaa1081f800628c30d2b0728f delivered 2017-12-06 12:04:06 2017-12-06 12:13:20 2017-12-07 20:28:28 2017-12-21 01:35:51 2018-01-04 87695ed086ebd36f20404c82d20fca87 36060 juiz de fora MG
42fdf880ba16b47b59251dd489d4441a 1 credit_card 2 128.45 15fd6fb8f8312dbb4674e4518d6fa3b3 delivered 2018-05-21 13:59:17 2018-05-21 16:14:41 2018-05-22 11:46:00 2018-06-01 21:44:53 2018-06-13 4291db0da71914754618cd789aebcd56 18570 conchas SP
298fcdf1f73eb413e4d26d01b25bc1cd 1 credit_card 2 96.12 a24e6f72471e9dbafcb292bc318f4859 delivered 2018-05-07 13:20:41 2018-05-07 15:31:14 2018-05-10 13:35:00 2018-05-14 19:02:54 2018-05-23 6e3c218d5f0434ddc4af3d6a60767bbf 13614 leme SP

The head of the dataframe confirms the result of the join.

Results

Grouping Parameter

The configurable param_vars_grouping parameter is populated with variables to be used to group the content of the df_customer_order_payment dataframe.

param_vars_grouping <- vars(customer_state, Purchase_year, Credit_Card)

Output

This is where we implement the functions previously described to create a presentation-ready summary table of the payment data contained in the df_customer_order_payment dataframe.

df_customer_order_payment_data <-
  df_customer_order_payment %>%
  
  # create a new boolean feature Credit_Card, defaulting all NULL values to
  # FALSE and make it `legible` using the func_legible_boolean function
  mutate(Credit_Card = coalesce(payment_type == "credit_card", FALSE)) %>%
  func_legible_boolean() %>%
  
  # simplify the order_purchase_timestamp to keep year only and store in a newly
  # created `Purchase_year` feature
  mutate_at("order_purchase_timestamp", funs(Purchase_year = year)) %>%
  
  # change all grouping variables to factor, grouping by all factors and
  # summarising payment_value for each combination of grouping variables
  mutate_at(param_vars_grouping, factor) %>%
  group_by_if(is.factor) %>%
  summarise_at("payment_value", sum, na.rm = TRUE) %>%
  ungroup() %>%
  
  # keep the top 3 customer_states ordered by desc payment_value total and lump
  # the other states into `Other`
  mutate(customer_state = fct_lump(f = customer_state, w = payment_value, n = 3)) %>%
  
  # regroup all factors based on the revised customer_state, and recalculate the
  # sum total for combinations
  group_by_if(is.factor) %>%
  summarise_if(is.numeric, sum, na.rm = TRUE) %>%
  
  # spread the payment values for each grouping combination by year
  spread(Purchase_year, payment_value) %>%
  ungroup() %>%
  
  # regroup and nest the summary content for each grouping combination
  group_by_if(is.factor) %>%
  nest() %>%
  
  # map each nested summary, gather the values and elongate the summary, summing
  # the content of the numeric field only
  mutate_at("data", funs(total = map), function(df) {
    df %>%
      gather() %>%
      summarise_if(is.numeric, sum, na.rm = TRUE) %>%
      
      # rename to Total as it will be added back into the grouping tibble when
      # unnested
      rename(Total = value) %>%
      
      # return the result
      return()
  }) %>%
  # unnest the newly created 'Total' feature
  unnest()

df_customer_order_payment_data %>% 
  
  # summarise each numeric column into sum totals
  func_create_summary() %>%
  
  # rename all headers by replacing placeholder underscores with spaces
  rename_all(func_present_headers) %>%
  
  # round all numeric values up to the nearest dollar and format value as
  # currency
  mutate_if(is.numeric, ~round(.) %>% scales::dollar(.)) %>%
  mutate_if(is.character, str_remove, pattern = "\\$NA") %>%
  
  # rename all factor values by replacing placeholder underscores with spaces
  mutate_if(is.factor, str_replace_all, pattern = "_", replacement = " ") %>%
  
  # align tabular values right and output as html
  kable(align = "r", format = "html") %>% 
  kableExtra::kable_styling(full_width = FALSE, 
                            font_size = 12) %>% 
  
  # merge duplicate rows into single cells (matplotlib style) to improve clarity
  kableExtra::collapse_rows(columns = 1, 
                            valign = "top") %>% 
  
  # add additional header categories
  kableExtra::add_header_above(c(
    "Dimension" = 2,
    "Year" = 3,
    " " = 1
  ))
Dimension
Year
Customer State Credit Card 2016 2017 2018 Total
MG Credit Card $4,832 $668,567 $798,576 $1,471,975
Not Credit Card $811 $186,324 $213,146 $400,282
RJ Credit Card $10,137 $849,698 $870,506 $1,730,341
Not Credit Card $3,271 $206,256 $204,512 $414,039
SP Credit Card $13,886 $1,972,256 $2,690,520 $4,676,662
Not Credit Card $2,999 $589,607 $728,958 $1,321,565
Other Credit Card $19,708 $2,146,853 $2,496,545 $4,663,106
Not Credit Card $3,718 $630,185 $696,999 $1,330,902
Total Total $59,362 $7,249,747 $8,699,763 $16,008,872

Another approach to output

This following section is a stylised alternative to the previous, creating separate summary tables for the boolean Credit Card payments options. The intention, in line with the theme of this article, is to improve clarity by breaking data into smaller, bite-size chunks.

The approach is instantiating the func_output_group_item_table function that takes as input a grouping parameter, and output the filtered results for distinct and consecutive categories within it.

func_output_group_item_table <-
  function(param_group, param_group_selector) {
    # print the distinct grouping category as header
    cat(paste0(
      "<h4>",
      str_replace_all(param_group, pattern = "_", replacement = " "),
      "</h4>"
    ))
    
    df_customer_order_payment_data %>%
      
      # filter the master data by the grouping category parameter
      filter(!!param_group_selector == param_group) %>%
      ungroup() %>%
      
      #  remove the grouping category from it
      select(-!!param_group_selector) %>%
      
      # create a row with a sum total for each numeric column
      func_create_summary() %>%
      
      # styling and presentation options executed next, as described in the
      # previous code block
      rename_all(func_present_headers) %>%
      mutate_if(is.numeric, ~ round(.) %>% scales::dollar(.)) %>%
      mutate_if(is.character, str_remove, pattern = "\\$NA") %>%
      mutate_if(is.factor,
                str_replace_all,
                pattern = "_",
                replacement = " ") %>%
      kable(align = "r", format = "html") %>%
      kableExtra::kable_styling(full_width = FALSE,
                                font_size = 12) %>%
      kableExtra::collapse_rows(columns = 1:4, valign = "middle") %>%
      return()
  }

func_output_group_item_table_iterator <-
  function(param_group_selector) {
    for (param_group in (df_customer_order_payment_data %>%
                         distinct(!!param_group_selector) %>%
                         pull()))  {
      func_output_group_item_table(param_group, param_group_selector) %>% print()
    }
  }

By Credit Card

# input the grouping parameter to output sectioned item tables
func_output_group_item_table_iterator(quo(Credit_Card))

Credit Card

Customer State 2016 2017 2018 Total
MG $4,832 $668,567 $798,576 $1,471,975
RJ $10,137 $849,698 $870,506 $1,730,341
SP $13,886 $1,972,256 $2,690,520 $4,676,662
Other $19,708 $2,146,853 $2,496,545 $4,663,106
Total $48,562 $5,637,374 $6,856,148 $12,542,084

Not Credit Card

Customer State 2016 2017 2018 Total
MG $811 $186,324 $213,146 $400,282
RJ $3,271 $206,256 $204,512 $414,039
SP $2,999 $589,607 $728,958 $1,321,565
Other $3,718 $630,185 $696,999 $1,330,902
Total $10,800 $1,612,373 $1,843,615 $3,466,788

By Customer State

# input the grouping parameter to output sectioned item tables
func_output_group_item_table_iterator(quo(customer_state))

MG

Credit Card 2016 2017 2018 Total
Credit Card $4,832 $668,567 $798,576 $1,471,975
Not Credit Card $811 $186,324 $213,146 $400,282
Total $5,643 $854,892 $1,011,723 $1,872,257

RJ

Credit Card 2016 2017 2018 Total
Credit Card $10,137 $849,698 $870,506 $1,730,341
Not Credit Card $3,271 $206,256 $204,512 $414,039
Total $13,408 $1,055,954 $1,075,018 $2,144,380

SP

Credit Card 2016 2017 2018 Total
Credit Card $13,886 $1,972,256 $2,690,520 $4,676,662
Not Credit Card $2,999 $589,607 $728,958 $1,321,565
Total $16,886 $2,561,863 $3,419,479 $5,998,227

Other

Credit Card 2016 2017 2018 Total
Credit Card $19,708 $2,146,853 $2,496,545 $4,663,106
Not Credit Card $3,718 $630,185 $696,999 $1,330,902
Total $23,426 $2,777,038 $3,193,544 $5,994,008

Summary

The demonstrated functions are invaluable in my workflow, without which the result would not be as effective nor consistent.

The functions are continuously evolving, becoming more configurable and robust while keeping the emphasis on simplicity and ease-of-use.

It shaves a lot of time off repetitive preparation and keeps me focused on the problem without the distraction of presentation.

Join the discussion below and add your tips and tricks when preparing to communicate results.


Session

sessionInfo()
## R version 3.5.1 (2018-07-02)
## Platform: x86_64-apple-darwin15.6.0 (64-bit)
## Running under: macOS  10.14
## 
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRlapack.dylib
## 
## locale:
## [1] en_GB.UTF-8/en_GB.UTF-8/en_GB.UTF-8/C/en_GB.UTF-8/en_GB.UTF-8
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] bindrcpp_0.2.2  knitr_1.20      lubridate_1.7.4 forcats_0.3.0  
##  [5] stringr_1.3.1   dplyr_0.7.8     purrr_0.2.5     readr_1.1.1    
##  [9] tidyr_0.8.2     tibble_1.4.2    ggplot2_3.1.0   tidyverse_1.2.1
## 
## loaded via a namespace (and not attached):
##  [1] tidyselect_0.2.5  xfun_0.4          haven_1.1.2      
##  [4] lattice_0.20-38   colorspace_1.3-2  viridisLite_0.3.0
##  [7] htmltools_0.3.6   emo_0.0.0.9000    yaml_2.2.0       
## [10] utf8_1.1.4        rlang_0.3.0.1     pillar_1.3.0     
## [13] withr_2.1.2       glue_1.3.0        selectr_0.4-1    
## [16] modelr_0.1.2      readxl_1.1.0      bindr_0.1.1      
## [19] plyr_1.8.4        munsell_0.5.0     blogdown_0.9     
## [22] gtable_0.2.0      cellranger_1.1.0  rvest_0.3.2      
## [25] kableExtra_0.9.0  evaluate_0.12     fansi_0.4.0      
## [28] highr_0.7         broom_0.5.0       Rcpp_1.0.0       
## [31] backports_1.1.2   scales_1.0.0      jsonlite_1.5     
## [34] hms_0.4.2         digest_0.6.18     stringi_1.2.4    
## [37] bookdown_0.7      grid_3.5.1        rprojroot_1.3-2  
## [40] cli_1.0.1         tools_3.5.1       magrittr_1.5     
## [43] lazyeval_0.2.1    crayon_1.3.4      pkgconfig_2.0.2  
## [46] xml2_1.2.0        assertthat_0.2.0  rmarkdown_1.10   
## [49] httr_1.3.1        rstudioapi_0.8    R6_2.3.0         
## [52] nlme_3.1-137      compiler_3.5.1

comments powered by Disqus