4

A recent update of packages means that this code for neatly making a tibble with tribble() (then used to make a table) no longer works.

library(dplyr)
table_data_1 <- tribble(~"Header1_A", ~"Header2_A",
                      0.19,          0.20,
                      "Header1_B",  "Header2_B",
                      0.19,          0.20)

The error message:

Error: Can't combine `Header1_1` <double> and `Header1_1` <character>.
Run `rlang::last_error()` to see where the error occurred.

And rlang::last_error() produces:

<error/vctrs_error_incompatible_type>
Can't combine `Header1_A` <double> and `Header1_A` <character>.
Backtrace:
 1. tibble::tribble(...)
 6. vctrs::vec_default_ptype2(...)
 7. vctrs::stop_incompatible_type(...)
 8. vctrs:::stop_incompatible(...)
 9. vctrs:::stop_vctrs(...)
Run `rlang::last_trace()` to see the full context.

Now that is probably one interpretation of reasonable that a recent update would make a default class for a column based on the first row of data, which in this case is then numeric. Then it is totally logical that it throws an error when I give it some text in the next row, which would be interpreted as character when it expects numeric. However, I'll note that tibble() has more sensible defaults (in my view), so I can create the desired tibble with a sensible default to character.

table_data_2 <- tibble(Header1_A = c(0.19, "Header1_B", 0.19), Header2_A = c(0.19, "Header2_B", 0.19))
table_data_2
# A tibble: 3 x 2
  Header1_A Header2_A
  <chr>     <chr>    
1 0.19      0.19     
2 Header1_B Header2_B
3 0.19      0.19  

However, it's not clear to me how I could retain the neat data entry of a tribble and override the default behaviour and specify that the columns should all be treated as character, without putting all the numbers in inverted commas - which would not give an identical tibble.

How can I get the tibble default behaviour (a sensible guess of class) with tribble, or specify the class?

EDIT Someone asked what my use case was. Basically I just wanted a neat tribble/tibble to demonstrate how you might go about making a table in rmarkdown that was a downloadable table in html output (with DT), or just neatly formatted in pdf/latex output with kableExtra). I live in hope that gt makes a lot of this stuff unnecessary!

---
title: "Untitled"
author: "me"
date: "22/05/2020"
output:
  # pdf_document:
  #   latex_engine: xelatex
  html_document: default
---

```{r setup, echo=FALSE, message=FALSE}
library(tidyverse)
table_data_2 <- tibble(Header1_A = c(0.19, "Header1_B", 0.19), Header2_A = c(0.19, "Header2_B", 0.19))
table_data_2


## Format table packages
library(kableExtra)
library(DT)

## Format for latex/PDF
my_table <- kable(table_data_2, format="latex", caption = "Dairy Farm Owner-operator Terms of Trade") %>%
  kable_styling(latex_options = c("striped", "hold_position")) %>%
  row_spec(c(0,2), bold = T) %>% 
  row_spec(0:(table_data_2 %>% 
                nrow()), extra_latex_after = "\\arrayrulecolor{gray}") %>% 
  row_spec(seq(1,table_data_2 %>% 
                 nrow(),2), background = "#daf7e1")

my_table #if latex output

## Format for html

#formatting the row stripes for datatables tables
callback <- c(
  "$('table.dataTable.display tbody tr:odd').css('background-color', '#b3ffd9');",
  "$('table.dataTable.display tbody tr:even').css('background-color', 'white');",
  "$('table.dataTable.display tbody tr:odd')",
  "  .hover(function(){",
  "    $(this).css('background-color', '#74e3ab');",
  "   }, function(){",
  "    $(this).css('background-color', '#b3ffd9');",
  "   }",
  "  );",
  "$('table.dataTable.display tbody tr:even')",
  "  .hover(function(){",
  "    $(this).css('background-color', '#d4d4d4');",
  "   }, function(){",
  "    $(this).css('background-color', 'white');",
  "   }",
  "  );"
)

formatStyle(
  datatable(table_data_2, extensions = 'Buttons',
            options=list(dom = 'Brt', buttons = c('excel','csv','print'),
                         pageLength=dim(table_data_2)[1],
                         ordering = FALSE),
            rownames = FALSE, 
            callback = JS(callback)
  ), 1, target = "row",
  fontWeight = styleEqual("Header1_B", "bold") )


```

For what its worth, image shows html on left, pdf/latex on right.

output

Mark Neal
  • 996
  • 16
  • 52
  • For your example, would it be acceptable to call `tribble()` twice, with minimal modification to existing code? – krlmlr May 21 '20 at 05:50
  • 1
    If I created two tribbles, it is not straightforward to join them into a single tibble - `bind_rows()` would need the same headers, and `rbind()` will lose the second set of headers. – Mark Neal May 21 '20 at 06:30
  • 1
    I created an issue at github for [tibble](https://github.com/tidyverse/tibble/issues/776) - it seems inconsistent that tribble gives a different result to tibble with the same data inputted in only a slightly different way, though maybe unavoidable? – Mark Neal May 21 '20 at 06:33
  • I suspect this is an XY problem, could you please show your use case? – krlmlr May 21 '20 at 15:01
  • You could just use `list()` instead of `tribble`; you're going to have to do a bit of manipulation in any case. – Simon Woodward May 21 '20 at 20:03
  • @krlmlr, I have updated example to show what my use case was, which demonstrates (in a waffly way) why it wasn't an XY problem - you might want any combination of text or numbers in your table, and row or column does not always define a class. – Mark Neal May 22 '20 at 07:32
  • 1
    @MarkNeal: From your use case I don't understand why you can't use two `kable()`s. Note how the numbers are left-aligned -- remove the "Header*_B" row to have them aligned correctly. The new strictness helps catch mistakes, I think in the vast majority of cases the new strictness does more good than harm. Use quotes if you really need strings. – krlmlr May 22 '20 at 08:40
  • Thanks, I’ll look into the multiple Kable approach and report back – Mark Neal May 22 '20 at 10:01

2 Answers2

1

If your use case is easily reading a table with unknown structure into a data frame, you can do it like this.

data <- list("Header1_A", "Header2_A",
             0.19,          0.20,
             "Header1_B",  "Header2_B",
             0.19,          0.20)

df <- as.data.frame(matrix(unlist(data), ncol = 2, byrow = TRUE), stringsAsFactors = FALSE)

df 
#>          V1        V2
#> 1 Header1_A Header2_A
#> 2      0.19       0.2
#> 3 Header1_B Header2_B
#> 4      0.19       0.2

Created on 2020-05-24 by the reprex package (v0.3.0)

To preserve column types (if possible) you could send the data list to this

dribble <- function(data, ncol){
  df <- list()
  for (i in 1:ncol){
    x <- unlist(data[seq(i, length(data), ncol)])
    df[[paste0("X", i)]] <- x
  }
  as.data.frame(df, stringsAsFactors = FALSE)
}

Simon Woodward
  • 1,946
  • 1
  • 16
  • 24
  • 1
    Thanks, that delivers for the use case. The undocumented change in tribble behaviour without an obvious method to revert to behaviour that tibble provides is still an issue for other users in my opinion. – Mark Neal May 23 '20 at 21:02
  • 1
    This forces _every_ column to the same type, which is usually something you don't want to do – Hong Ooi May 23 '20 at 21:06
  • I was reading that `tibble`, and by extension `tribble`, is designed to be *more* fussy that the base R functions, so it doesn't surprise me that it is less tolerant of inconsistent data, to push back possible mistakes to the user. – Simon Woodward May 23 '20 at 21:06
  • @HongOoi true, you could write a loop to extract each column from the list and preserve type (if consistent). – Simon Woodward May 23 '20 at 21:09
0

You could just use list() instead of tribble; you're going to have to do a bit of manipulation in any case.

library(dplyr)
library(tidyr)

data <- list("Header1_A", "Header2_A",
                        0.19,          0.20,
                        "Header1_B",  "Header2_B",
                        0.19,          0.20)
type <- lapply(data, typeof)

df <- data.frame(
  headers = unlist(data[unlist(type) == "character"]),
  values = unlist(data[unlist(type) == "double"])
)

df %>% 
  separate(headers, into = c("header", "group"), sep = "_") %>% 
  pivot_wider(id_cols = group, names_from = header, values_from = values)
#> # A tibble: 2 x 3
#>   group Header1 Header2
#>   <chr>   <dbl>   <dbl>
#> 1 A        0.19     0.2
#> 2 B        0.19     0.2

Created on 2020-05-22 by the reprex package (v0.3.0)

Simon Woodward
  • 1,946
  • 1
  • 16
  • 24
  • I think the use case I've outlined shows why this won't solve my problem, but some neat programming nonetheless! – Mark Neal May 22 '20 at 07:33
  • Close: tribble recently *stopped* working like I want it to. If I’d set up my demos with tibble instead of tribble, I would never have known. There are good reasons to use tribble, such as clarity of code. But having it break for reasons like change in defaults begs the question of whether defaults can be changed, which is why I raised a GitHub issue too. – Mark Neal May 23 '20 at 20:45