Tibbles: A Modern Data Frame

Optional Content

This page is a brief preview of an advanced topic that you’ll encounter in later modules, not a required part of this module’s learning outcomes. Feel free to skim the following information now and come back later once you’ve had more practice with ordinary data frames.

As you continue working with R—especially once you start using packages from the tidyverse like dplyr and ggplot2—you’ll frequently encounter a special kind of data frame called a tibble. This page provides a brief introduction to these very handle data objects.

Creating Tibbles

Tibbles are provided by the tibbles package, not Base R. So, before you can use tibbles, you must first install the tibble package.

install.packages("tibble")

We create tibbles with the tibble() function, which works (mostly) like data.frame().

library(tibble)

t1 <- tibble(
  x = rnorm(10),
  y = sample(c("foo", "bar", "baz"), 10, TRUE),
  z = x^2
)
t1
# A tibble: 10 × 3
         x y           z
     <dbl> <chr>   <dbl>
 1 -0.762  bar   0.581  
 2  0.243  foo   0.0589 
 3  0.364  baz   0.132  
 4  0.398  bar   0.158  
 5 -0.966  bar   0.933  
 6 -0.0858 baz   0.00736
 7 -0.725  baz   0.525  
 8  0.676  foo   0.457  
 9  0.435  baz   0.189  
10 -1.53   baz   2.34   

Notice how the printed output looks a little different from an ordinary data frame. Tibbles print the number of rows and columns and show each column’s type under the column name. When printing large tibbles, the rows and/or columns will be truncated into a sensibly sized summary.

as_tibble(
  matrix(
    rnorm(10000),
    ncol = 50,
    dimnames = list(NULL, paste0("x", 1:50))
  )
)
# A tibble: 200 × 50
       x1      x2     x3     x4      x5      x6     x7       x8       x9    x10
    <dbl>   <dbl>  <dbl>  <dbl>   <dbl>   <dbl>  <dbl>    <dbl>    <dbl>  <dbl>
 1 -0.186  0.791  -0.797  0.450  0.537  -0.129   1.44  -0.666   -0.834    0.603
 2 -0.594  1.20    0.807 -1.81   0.715  -1.12   -0.214 -0.767    2.28     0.132
 3  1.05  -0.0889 -0.411  1.42  -0.0947 -0.233  -0.265  0.892   -1.35     1.51 
 4 -0.436 -1.97    0.471 -0.566  0.497   0.174  -1.25  -1.05    -0.0398   1.56 
 5  0.917 -0.0285 -0.881 -0.881 -1.29   -0.0773  0.869  0.230    0.651   -0.674
 6 -0.289  2.56   -1.74  -1.71  -0.768  -0.480   1.02   0.967    0.809    0.696
 7 -0.243  0.818  -0.798  0.692  1.57    1.09   -0.848 -0.00499 -1.82     0.339
 8  1.33   1.57    0.470 -0.169  1.03    0.189   0.937  0.140   -0.00866 -1.83 
 9 -0.553 -0.316  -0.466 -1.63  -0.161   1.78   -1.16  -0.971   -1.11     1.12 
10  1.89   1.21    2.62  -0.893  1.31    1.10    0.575 -0.757   -0.599    0.438
# ℹ 190 more rows
# ℹ 40 more variables: x11 <dbl>, x12 <dbl>, x13 <dbl>, x14 <dbl>, x15 <dbl>,
#   x16 <dbl>, x17 <dbl>, x18 <dbl>, x19 <dbl>, x20 <dbl>, x21 <dbl>,
#   x22 <dbl>, x23 <dbl>, x24 <dbl>, x25 <dbl>, x26 <dbl>, x27 <dbl>,
#   x28 <dbl>, x29 <dbl>, x30 <dbl>, x31 <dbl>, x32 <dbl>, x33 <dbl>,
#   x34 <dbl>, x35 <dbl>, x36 <dbl>, x37 <dbl>, x38 <dbl>, x39 <dbl>,
#   x40 <dbl>, x41 <dbl>, x42 <dbl>, x43 <dbl>, x44 <dbl>, x45 <dbl>, …

This bespoke print method is one difference between tibbles and traditional data frames, but there are several others (including a few very handy improvements).

How Tibbles Differ from Data Frames

In many ways, tibbles don’t differ from data frames. For the most part, tibbles are meant to be a drop-in replacement for traditional data frames, so they don’t change much about the core data frame features (after all, data frames are already very useful). As far as R is concerned, tibbles are still data frames underneath.

is.data.frame(t1)
[1] TRUE

Since they share the same underlying structure, you can seamlessly convert data frames to tibbles and visa-versa.

# Convert a tibble to an ordinary data frame
(d1 <- as.data.frame(t1))
             x   y           z
1  -0.76229317 bar 0.581090875
2   0.24265368 foo 0.058880807
3   0.36365013 baz 0.132241415
4   0.39760654 bar 0.158090965
5  -0.96581685 bar 0.932802184
6  -0.08579004 baz 0.007359931
7  -0.72475045 baz 0.525263208
8   0.67622734 foo 0.457283416
9   0.43528651 baz 0.189474349
10 -1.53130589 baz 2.344897743
# Convert a data frame to a tibble
as_tibble(d1)
# A tibble: 10 × 3
         x y           z
     <dbl> <chr>   <dbl>
 1 -0.762  bar   0.581  
 2  0.243  foo   0.0589 
 3  0.364  baz   0.132  
 4  0.398  bar   0.158  
 5 -0.966  bar   0.933  
 6 -0.0858 baz   0.00736
 7 -0.725  baz   0.525  
 8  0.676  foo   0.457  
 9  0.435  baz   0.189  
10 -1.53   baz   2.34   

The goal of tibbles is to smooth over some of the confusing, frustrating, or surprising behavior of traditional data frames. Below, we highlight a few of the most useful differences.

Tibbles Don’t Do Surprise Simplification

Remember how selecting a single column from a data frame using matrix-style indexing (d1[, 1]) automatically simplifies the result to a vector? I certainly hope so; we just covered that on the last page. Anyway, tibbles don’t do that.

Subsetting a tibble using matrix-style indexing always returns a tibble, regardless of how many rows or columns are extracted.

t1[1:2, 1:2]
# A tibble: 2 × 2
       x y    
   <dbl> <chr>
1 -0.762 bar  
2  0.243 foo  
t1[, 1]
# A tibble: 10 × 1
         x
     <dbl>
 1 -0.762 
 2  0.243 
 3  0.364 
 4  0.398 
 5 -0.966 
 6 -0.0858
 7 -0.725 
 8  0.676 
 9  0.435 
10 -1.53  
t1[1, ]
# A tibble: 1 × 3
       x y         z
   <dbl> <chr> <dbl>
1 -0.762 bar   0.581

Tibbles Don’t Do Partial Name Matching

We haven’t explicitly addressed it yet, but many R commands use “partial matching” to resolve your inputs. For example, we don’t actually have to write out the full word digits to specify the corresponding argument in round(). We only need to provide enough of the initial letters to disambiguate the intended argument name.

round(digits = 2, 3.14159)
[1] 3.14
round(dig = 2, 3.14159)
[1] 3.14
round(d = 2, 3.14159)
[1] 3.14

Ordinary data frames use the system to match column names. So, to match the column foo in the following data frame, we only need to specify one letter.

d1 <- data.frame(
  foo = rnorm(10),
  bar = runif(10),
  baz = rbinom(10, 1, 0.5)
)

d1$f
 [1]  1.83739803 -1.23610049  1.05058743 -0.23322273 -0.33858215 -0.07594070
 [7] -1.59757910 -1.90550864 -1.06889178 -0.07472366

To match the bar or baz columns, however, we need to give the full column name, since these two names only differ in their final letter.

d1$ba
NULL

Usually, this behavior doesn’t cause problems, but it creates unnecessary ambiguity, and increases the opportunities for bugs. Consider the following hypothetical data frame.

myData <- data.frame(
  female = sample(0:1, 20, TRUE),
  malevolent = sample(0:1, 20, TRUE),
  score = rnorm(20)
)

myData
   female malevolent      score
1       1          1  0.4898893
2       1          1  1.3149508
3       0          1 -0.7239994
4       0          0 -0.2123139
5       0          1  0.1404637
6       1          0  0.8564879
7       0          1 -0.7082696
8       1          1 -0.7540027
9       1          0  0.8148100
10      0          1  0.3548326
11      1          1 -1.2906852
12      1          0 -1.4778001
13      1          1 -0.7545866
14      1          0  0.5335948
15      0          0 -1.5430491
16      0          0  2.0755362
17      0          0 -2.1070245
18      1          0 -1.3316309
19      0          0  0.1616109
20      0          1 -2.1162149

Suppose the female column contains a dummy code representing biological sex. Further suppose that I want to select the female column, but I misremember its name as male, so I run the following command.

myData$male
 [1] 1 1 1 0 1 0 1 1 0 1 1 0 1 0 0 0 0 0 0 1

Oops…hope I’m paying extra close attention. Otherwise, as I continue with my analysis, the cases that I think are biological males are actually the cases that have been flagged as “malevolent” by the dummy code that I accidentally selected via silent partial matching.

Tibbles don’t allow partial matching for columns names, so this type of mistake cannot happen when working with a tibble.

myData2 <- as_tibble(myData)

myData2$male
Warning: Unknown or uninitialised column: `male`.
NULL

Tibbles Dramatically Restrict Recycling

Tibbles do away with essentially all of the convoluted recycling rules we’ve discussed for other type of R objects. Basically, we’re only allowed to recycle length-one vectors. So, the following commands are all accepted.

myData2$female <- 42
myData2
# A tibble: 20 × 3
   female malevolent  score
    <dbl>      <int>  <dbl>
 1     42          1  0.490
 2     42          1  1.31 
 3     42          1 -0.724
 4     42          0 -0.212
 5     42          1  0.140
 6     42          0  0.856
 7     42          1 -0.708
 8     42          1 -0.754
 9     42          0  0.815
10     42          1  0.355
11     42          1 -1.29 
12     42          0 -1.48 
13     42          1 -0.755
14     42          0  0.534
15     42          0 -1.54 
16     42          0  2.08 
17     42          0 -2.11 
18     42          0 -1.33 
19     42          0  0.162
20     42          1 -2.12 
myData2[1:6, "score"] <- pi
myData2
# A tibble: 20 × 3
   female malevolent  score
    <dbl>      <int>  <dbl>
 1     42          1  3.14 
 2     42          1  3.14 
 3     42          1  3.14 
 4     42          0  3.14 
 5     42          1  3.14 
 6     42          0  3.14 
 7     42          1 -0.708
 8     42          1 -0.754
 9     42          0  0.815
10     42          1  0.355
11     42          1 -1.29 
12     42          0 -1.48 
13     42          1 -0.755
14     42          0  0.534
15     42          0 -1.54 
16     42          0  2.08 
17     42          0 -2.11 
18     42          0 -1.33 
19     42          0  0.162
20     42          1 -2.12 
myData2[1:2] <- list("foo", FALSE)
myData2
# A tibble: 20 × 3
   female malevolent  score
   <chr>  <lgl>       <dbl>
 1 foo    FALSE       3.14 
 2 foo    FALSE       3.14 
 3 foo    FALSE       3.14 
 4 foo    FALSE       3.14 
 5 foo    FALSE       3.14 
 6 foo    FALSE       3.14 
 7 foo    FALSE      -0.708
 8 foo    FALSE      -0.754
 9 foo    FALSE       0.815
10 foo    FALSE       0.355
11 foo    FALSE      -1.29 
12 foo    FALSE      -1.48 
13 foo    FALSE      -0.755
14 foo    FALSE       0.534
15 foo    FALSE      -1.54 
16 foo    FALSE       2.08 
17 foo    FALSE      -2.11 
18 foo    FALSE      -1.33 
19 foo    FALSE       0.162
20 foo    FALSE      -2.12 

These commands, on the other hand, will all fail because we’re trying to recycle vectors with more than one element.

myData2$female <- 40:42
Error in `$<-`:
! Assigned data `40:42` must be compatible with existing data.
✖ Existing data has 20 rows.
✖ Assigned data has 3 rows.
ℹ Only vectors of size 1 are recycled.
Caused by error in `vectbl_recycle_rhs_rows()` at tibble/R/subassign-backend.R:51:5:
! Can't recycle input of size 3 to size 20.
myData2[1:6, "score"] <- c(pi, NA)
Error in `[<-`:
! Assigned data `c(pi, NA)` must be compatible with row subscript `1:6`.
✖ 6 rows must be assigned.
✖ Assigned data has 2 rows.
ℹ Only vectors of size 1 are recycled.
Caused by error in `vectbl_recycle_rhs_rows()` at tibble/R/subassign-backend.R:206:7:
! Can't recycle input of size 2 to size 6.
myData2[1:2] <- list("foo", c(FALSE, TRUE))
Error in `[<-`:
! Assigned data `list("foo", c(FALSE, TRUE))` must be compatible with
  existing data.
✖ Existing data has 20 rows.
✖ Element 2 of assigned data has 2 rows.
ℹ Only vectors of size 1 are recycled.
Caused by error in `vectbl_recycle_rhs_rows()` at tibble/R/subassign-backend.R:51:5:
! Can't recycle input of size 2 to size 20.
Knowledge Check
  • How are tibbles and data frames related?
  • What’s the purpose of tibbles, when we already have perfectly serviceable data frames?
  • Name one specific behavior where tibbles differ from ordinary data frames, and explain the pros and cons of that difference.
Ready to Continue?

You’re ready to progress if you can:

  • Create a data frame with columns of different types.
  • Explain how a data frames are related to lists and matrices.
  • Use $, [], [[]], and matrix-style subsetting to access and modify data frame contents.
  • Explain the differences between the four subsetting operators.
  • Explain how recycling applies when modifying data frame columns.

Take the Knowledge Quiz to evaluate your learning.

Back to top