Code
x <- 2.0
attr(x, "Mood") <- "Haha!"Claudius Gräbner-Radkowitsch
June 16, 2026
June 16, 2026
The second part of the session 4 video covers advanced object types (factors and data frames):
In the previous tutorials you learned about the most important fundamental data types in R. The types we will learn about below are not less important, but less fundamental. This means they are built by taking one of the base types we encountered before, and ‘adding some features’. These features change the behavior of the type, e.g. how it is printed or how it is affected by certain function calls, but also what kind of operations it allows.
This process of ‘adding features’ is usually done by adding ‘attributes’ to an object. In principle, you can add attributes to any objects without much effect by using the attr() function:
To retrieve attributes use attributes():
Sometimes, adding attributes of a particular name have more relevant implications. One useful way to use attributes, for instance, is to name the single elements of vectors, something that changes the way the objects are printed and something that we already discussed in the context of lists:
Now we can call elements by their name:
But things become really interesting if you add an attribute called class: this really transforms the data types into a new, less fundamental type. In fact, this is how the types we discuss below are created: smart people added, among other things, a class attribute to a more fundamental data type (integer in the case of factors and list in the case of data.frames).
One implication of this ‘less fundamental’ nature of the objects we encounter below is that typeof() usually returns the base type. For instance, below we will learn about the factor, a type that is built upon integer. If we call typeof() on a factor, it will return the fundamental type, i.e. integer:
Fortunately, the standard test functions (is.*()) usually work, so you can use is.factor():
Alternatively, you can always inspect the attributes of the object to find out about its class:
All this can be confusing at first, so it is important to keep it in mind. Once you wrapped your head around this, many confusing behaviors suddenly start to make sense, e.g. that mutating factors within a data.frame can result in whole numbers, a phenomenon we will discuss in the context of data wrangling later.
We usually use factors to represent ordinal or categorical data. At first sight a factor is an atomic vector that can only take a pre-specified number of values, so called levels. To create a factor we use the function - surprise - factor():
[1] Female Male Female
Levels: Female Male
If we want to define levels that do not yet have any instances, we can do this with the optional argument levels:
[1] Female Male Female
Levels: Diverse Female Male
If we use the argument levels and also try to add values that are not included in the levels, these are set to NA:
[1] Female Male Female <NA>
Levels: Female Male
Usually, the sequence in which we mention levels does not matter. In the case of ordinal data, however, the sequence becomes important. To consider it, we must use the argument ordered:
[1] High High Low High
Levels: Low < Mid < High
Frequently, the elements of factors are words, i.e. objects of the type character. From a technical point of view, however, factors are stored as integers: in order to save memory space, each level is assigned a whole number in the computer memory, which is then mapped to the actual value. Especially when the elements consist of large numbers or long words, this helps saving memory space since these expressions only have to be stored once, and each element of the factor is only a simple number. Therefore, typeof() also returns integer for factors:
[1] "integer"
To check whether an object is a factor we use the function is.factor():
Not all operations that are defined for integers, however, also work for factors:
At the same time, there are some useful things we can do with factors. For instance, the function table() gives us the absolute frequencies of the factor elements, a task that is very common for categorical data:
The data.frame is a special type of list. It is among the most widespread data types used in data science. In contrast to a normal list, all elements of a data.frame must have the same length. This means that you can think of a data.frame as a list arranged as a rectangle and represented as a table. The headings of the tables then correspond to the names of the vector elements of the list.
Because of the close relationship, we can create a data.frame directly from a list by using the function as.data.frame():
Here we might think of the names of the three vectors, a, b and c as the headings of a table (representing, often, variable names), and the content of the vectors as the cell entries of the table:
The relation to lists becomes obvious if we call typeof(), which returns the underlying data type:
To test whether an object is a data.frame we use is.data.frame:
The difference to a list becomes particularly obvious in the different printing behavior of the two:
A more direct way to create a data.frame is to use the function data.frame():
gender height
1 male 189
2 male 175
3 male 180
4 female 166
5 female 150
To extract single elements, columns or rows of a data.frame we can use [ and [[ in a similar way we used it for lists, just keeping in mind that we now have two dimensions to deal with, instead of only one in the case of lists.
To subset columns we can call them by name:
The result is another data.frame:
If we want to extract the underlying atomic vector we must use [[:
This is also helpful to inspect the underlying data type:
Data frames can become very large, so it is often useful to get a first overview about what the data.frame contains. The functions names(), dplyr::glimpse(), head(), and View() are useful in this context.
The function names() returns a vector with all the column names:
Hint: For large
data.frames it can be useful to wrapnames()intosort(), which sorts the resulting vector in alphabetical order.
To print the first n rows of a data.frame use head(). By default n=5, but you can set it explicitly:
The package dplyr provides the useful function dplyr::glimpse(), which gives a general overview about all columns and their types:
Finally, if you work within R-Studio, you can use View() to get an Excel-like representation of your data.
As you might have guessed by now, data.frames are the classic object to represent read-in data. If, for instance, you download data on GDP in Germany from the Internet and then import this data into R, it usually gets represented as a data.frame. This representation then allows direct analysis and manipulation of the data.
At the same time, data.frames are a very old object type, the behavior of which sometimes feels a bit outdated. Because of this, some authors created a new data type, which is built upon the data.frame, but behaves slightly different: the tibble. The tibble is made available via the package tibble, so to use tibbles, this package has to be installed first.
You can read more about tibbles in Chapter 10 of R for Data Science.
Digression: Dialects of R As with natural languages, programming languages feature different dialects, i.e. different ways of doing the same actions. In
Rthis becomes particularly obvious in the context of dealing with data. Here, the different dialects show themselves in different object types used for the data you are working with. To the best of our knowledge, there are three main dialects, each associated with a different focus on data representation. The base dialect represents data asdata.frames and it is the original approach envisioned by theRdevelopers. The tidyverse dialect represents data astibbles and is built around a number of different packages summarized under the name tidyverse. It is aimed to develop a set of packages that allow you to do all tasks commonly associated with data science within a consistent syntax and design philosophy. The data.table dialect represents data asdata.tables, which are provided by the package data.table. It is designed to work with large data sets and is by far the fastest and computationally most efficient approach to data processing.
Another data type that is used frequently in many circumstances, especially statistics and engineering, are matrices. These are two-dimensional objects with rows and columns, each of which are atomic vectors. Matrices are created with the function matrix(). This function takes as its first argument the elements of the matrix and then the specification of the number of rows (nrow) and/or the number of columns (ncol):
[,1] [,2]
[1,] 11 16
[2,] 12 17
[3,] 13 18
[4,] 14 19
[5,] 15 20
We can extract and/or substitute the rows and columns as well as individual values as follows:
To test whether an object is a matrix use is.matrix():
The ObjectTypes2 exercise set in the DataScienceExercises package covers the main concepts from this tutorial:
See the blog post on DataScienceExercises for installation instructions.
@inreference{graebner2026-r-object-types-iii-factors-dataframes,
author = {Gräbner-Radkowitsch, Claudius},
editor = {Gräbner-Radkowitsch, Claudius},
title = {Object {Types} in {R} {III:} {Factors} and {Data} {Frames}},
booktitle = {Method and Inquiry},
date = {2026-06-16},
url = {https://euf-methodology-hub.netlify.app/resources/r-object-types-iii-factors-dataframes},
langid = {en}
}
---
title: "Object Types in R III: Factors and Data Frames"
description: "An introduction to factors and data frames in R: how they are built upon base types, how to create and manipulate them, and the tibble as a modern alternative."
author: "Claudius Gräbner-Radkowitsch"
date: 2026-06-16
date-modified: 2026-06-16
image: ../assets/images/r-programming.png
learning-objectives:
- "Create and manipulate factors, including ordered factors for ordinal data"
- "Build data frames and tibbles from lists and vectors"
- "Extract rows, columns, and individual elements from a data frame"
- "Explain the relationship between lists, data frames, and tibbles"
categories:
- R
- tutorial
- BA
level:
- BA
content-type: tutorial
citation:
id: graebner2026-r-object-types-iii-factors-dataframes
type: entry-encyclopedia
container-title: "Method and Inquiry"
title: "Object Types in R III: Factors and Data Frames"
editor:
- name: "Claudius Gräbner-Radkowitsch"
url: https://euf-methodology-hub.netlify.app/resources/r-object-types-iii-factors-dataframes
---
```{=html}
<div class="track-nav">
<span class="track-nav-label">Getting Started with R</span>
<span class="track-nav-position">Step 6 of 7</span>
<span class="track-nav-links">← <a href="r-object-types-ii-vectors.qmd">Object Types in R II: Vectors</a>  ·  <a href="setting-up-an-r-project.qmd">Setting Up an R Project</a> →</span>
<a href="../tracks/getting-started-with-r.qmd" class="track-nav-full">View full track →</a>
</div>
```
# Video lecture {.unnumbered}
The second part of the session 4 video covers advanced object types (factors and data frames):
```{=html}
<div class="embed-container">
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/videoseries?si=2Dpn5w5dNSh4yw5y&list=PLZDawQMrG1RK0Ia3Y3AFqf_FP59GJnIP7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>
```
# Optional background info: attributes and classes
In the previous tutorials you learned about the most important fundamental data
types in `R`.
The types we will learn about below are not less important, but less fundamental.
This means they are built by taking one of the base types we encountered before,
and 'adding some features'. These features change the behavior of the type,
e.g. how it is printed or how it is affected by certain function calls, but
also what kind of operations it allows.
This process of 'adding features' is usually done by adding 'attributes' to
an object. In principle, you can add attributes to any objects without
much effect by using the `attr()` function:
```{r}
x <- 2.0
attr(x, "Mood") <- "Haha!"
```
To retrieve attributes use `attributes()`:
```{r}
attributes(x)
```
Sometimes, adding attributes of a particular name have more relevant
implications.
One useful way to use attributes, for instance, is to name the single elements
of vectors,
something that changes the way the objects are printed and something that we
already discussed in the context of lists:
```{r}
y <- c(1, 2, 3)
attr(y, "names") <- c("First", "Second", "Third")
y
```
```{r}
attributes(y)
```
Now we can call elements by their name:
```{r}
y["Second"]
```
But things become really interesting if you add an attribute called `class`:
this really transforms the data types into a new, less fundamental type. In
fact, this is how the types we discuss below are created: smart people added,
among other things, a class attribute to a more fundamental data type
(`integer` in the case of `factor`s and `list` in the case of `data.frame`s).
One implication of this 'less fundamental' nature of the objects we encounter
below is that `typeof()` usually returns the base type. For instance, below
we will learn about the `factor`, a type that is built upon `integer`.
If we call `typeof()` on a `factor`, it will return the fundamental type,
i.e. `integer`:
```{r}
xx <- factor(c(1,2))
typeof(xx)
```
Fortunately, the standard test functions (`is.*()`) usually work, so you can
use `is.factor()`:
```{r}
is.factor(xx)
```
Alternatively, you can always inspect the attributes of the object to find
out about its class:
```{r}
attributes(xx)
```
All this can be confusing at first, so it is important to keep it in mind. Once
you wrapped your head around this, many confusing behaviors suddenly start to make
sense, e.g. that mutating factors within a `data.frame` can result in whole
numbers, a phenomenon we will discuss in the context of data wrangling later.
# Factors {#introfactors}
We usually use factors to represent ordinal or categorical data.
At first sight a factor is an atomic vector that can only take a pre-specified
number of values, so called *levels*.
To create a factor we use the function - surprise - `factor()`:
```{r}
x <- c("Female", "Male", "Female")
x <- factor(c("Female", "Male", "Female"))
x
```
If we want to define levels that do not yet have any instances, we can do this
with the optional argument `levels`:
```{r}
x <- c("Female", "Male", "Female")
x <- factor(c("Female", "Male", "Female"),
levels=c("Diverse","Female", "Male"))
x
```
If we use the argument levels and also try to add values that are not included
in the levels, these are set to `NA`:
```{r}
x <- c("Female", "Male", "Female")
x <- factor(c("Female", "Male", "Female", "Diverse"),
levels=c("Female", "Male"))
x
```
Usually, the sequence in which we mention levels does not matter.
In the case of ordinal data, however, the sequence becomes important.
To consider it, we must use the argument `ordered`:
```{r}
x <- c("High", "High", "Low", "High")
x <- factor(x,
levels = c("Low", "Mid", "High"),
ordered = TRUE)
x
```
Frequently, the elements of factors are words, i.e. objects of the type
`character`.
From a technical point of view, however, factors are stored as `integers`:
in order to save memory space, each level is assigned a whole number in the
computer memory, which is then mapped to the actual value.
Especially when the elements consist of large numbers or long words, this helps
saving memory space since these expressions only have to be stored once, and
each element of the factor is only a simple number.
Therefore, `typeof()` also returns `integer` for factors:
```{r}
x <- factor(c("Female", "Male", "Female"),
levels=c("Male", "Female", "Diverse"))
typeof(x)
```
To check whether an object is a factor we use the function `is.factor()`:
```{r}
is.factor(x)
```
Not all operations that are defined for `integer`s, however, also work for
`factor`s:
```{r}
x[1] + x[2]
```
At the same time, there are some useful things we can do with factors.
For instance, the function `table()` gives us the absolute frequencies of the
factor elements, a task that is very common for categorical data:
```{r}
table(x)
```
# Data Frames
The `data.frame` is a special type of list. It is among the most widespread
data types used in data science.
In contrast to a normal list, all elements of a `data.frame` must have the same
length.
This means that you can think of a `data.frame` as a list arranged as a
rectangle and represented as a table. The headings of the tables then
correspond to the names of the vector elements of the list.
Because of the close relationship, we can create a `data.frame` directly
from a list by using the function `as.data.frame()`:
```{r}
l_3 <- list(
"a" = 1:3,
"b" = 4:6,
"c" = 7:9
)
df_3 <- as.data.frame(l_3)
```
Here we might think of the names of the three vectors, `a`, `b` and `c` as the
headings of a table (representing, often, variable names), and the content of
the vectors as the cell entries of the table:
```{r}
df_3
```
The relation to `list`s becomes obvious if we call `typeof()`, which returns
the underlying data type:
```{r}
typeof(df_3)
```
To test whether an object is a `data.frame` we use `is.data.frame`:
```{r}
is.data.frame(df_3)
```
```{r}
is.data.frame(l_3)
```
The difference to a list becomes particularly obvious in the different
printing behavior of the two:
```{r}
l_3
```
```{r}
df_3
```
A more direct way to create a `data.frame` is to use the function
`data.frame()`:
```{r}
df_4 <- data.frame(
"gender" = c(rep("male", 3), rep("female", 2)),
"height" = c(189, 175, 180, 166, 150)
)
df_4
```
To extract single elements, columns or rows of a `data.frame` we can use
`[` and `[[` in a similar way we used it for lists, just keeping in mind that
we now have two dimensions to deal with, instead of only one in the case of lists.
To subset columns we can call them by name:
```{r}
df_4["gender"]
```
The result is another `data.frame`:
```{r}
is.data.frame(df_4["gender"])
```
If we want to extract the underlying atomic vector we must use `[[`:
```{r}
df_4[["gender"]]
```
This is also helpful to inspect the underlying data type:
```{r}
typeof(df_4[["gender"]])
```
Data frames can become very large, so it is often useful to get a first overview
about what the `data.frame` contains. The functions `names()`, `dplyr::glimpse()`,
`head()`, and `View()` are useful in this context.
The function `names()` returns a vector with all the column names:
```{r}
names(df_4)
```
> **Hint:** For large `data.frame`s it can be useful to wrap `names()` into `sort()`, which
sorts the resulting vector in alphabetical order.
To print the first `n` rows of a `data.frame` use `head()`.
By default `n=5`, but you can set it explicitly:
```{r}
head(df_4, n = 2)
```
The package `dplyr` provides the useful function `dplyr::glimpse()`, which
gives a general overview about all columns and their types:
```{r, eval=FALSE}
dplyr::glimpse(df_4)
```
Finally, if you work within R-Studio, you can use `View()` to get an Excel-like
representation of your data.
As you might have guessed by now, `data.frame`s are the classic object to
represent read-in data.
If, for instance, you download data on GDP in Germany from the Internet and then
import this data into `R`, it usually gets represented as a `data.frame`.
This representation then allows direct analysis and manipulation of the data.
At the same time, `data.frame`s are a very old object type, the behavior of
which sometimes feels a bit outdated. Because of this, some authors created
a new data type, which is built upon the `data.frame`, but behaves slightly
different: the `tibble`. The tibble is made available via the package `tibble`,
so to use tibbles, this package has to be installed first.
You can read more about tibbles in [Chapter 10 of R for Data Science](https://r4ds.had.co.nz/tibbles.html).
> **Digression: Dialects of R** As with natural languages, programming languages
feature different dialects, i.e. different ways of doing the same actions. In
`R` this becomes particularly obvious in the context of dealing with data. Here,
the different dialects show themselves in different object types used for the
data you are working with. To the best of our knowledge, there are three main
dialects, each associated with a different focus on data representation.
The *base* dialect represents data as `data.frame`s and it is the original
approach envisioned by the `R` developers. The *tidyverse* dialect
represents data as `tibble`s and is built around a number of different packages
summarized under the name [tidyverse](https://www.tidyverse.org/). It is aimed
to develop a set of packages that allow you to do all tasks commonly associated
with data science within a consistent syntax and design philosophy.
The *data.table* dialect represents data as `data.table`s, which are provided by
the package [data.table](https://github.com/Rdatatable/data.table). It is
designed to work with large data sets and is by far the fastest and computationally
most efficient approach to data processing.
# Digression: Matrices in R {#intro-matrix}
Another data type that is used frequently in many
circumstances, especially statistics and engineering, are
matrices. These are two-dimensional objects with rows and columns,
each of which are atomic vectors.
Matrices are created with the function `matrix()`.
This function takes as its first argument the elements of the matrix and then
the specification of the number of rows (`nrow`) and/or the number of
columns (`ncol`):
```{r}
m_1 <- matrix(11:20, nrow = 5)
m_1
```
We can extract and/or substitute the rows and columns as well as
individual values as follows:
```{r}
m_1[,1] # First column
```
```{r}
m_1[1,] # First row
```
```{r}
m_1[2,2] # Element [2,2]
```
To test whether an object is a matrix use `is.matrix()`:
```{r}
is.matrix(m_1)
```
# Practice
The `ObjectTypes2` exercise set in the `DataScienceExercises` package covers the
main concepts from this tutorial:
```{r, eval=FALSE}
learnr::run_tutorial(
name = "ObjectTypes2",
package = "DataScienceExercises",
shiny_args = list("launch.browser" = TRUE)
)
```
See the [blog post on DataScienceExercises](../blog/2026-06-16-datascienceexercises/index.qmd)
for installation instructions.
# Further reading
- [Chapter 16](https://r4ds.hadley.nz/factors) in R for Data Science
- [Chapter 10](https://r4ds.had.co.nz/tibbles.html) on tibbles in R for Data Science
<script src="https://giscus.app/client.js"
data-repo="graebnerc/research-methodology-hub"
data-repo-id="R_kgDOS7J-4w"
data-category="Comments"
data-category-id="DIC_kwDOS7J-484C_N51"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="en"
crossorigin="anonymous"
async>
</script>