install.packages("tibble")Tibbles: A Modern Data Frame
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.
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$baNULL
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$maleWarning: 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:42Error 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.
- 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.
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.