Object Types in R III: Factors and Data Frames

R
tutorial
BA
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

Published

June 16, 2026

Modified

June 16, 2026

Getting Started with R Step 6 of 7 ← Object Types in R II: Vectors  ·  Setting Up an R Project → View full track →

Video lecture

The second part of the session 4 video covers advanced object types (factors and data frames):

1 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:

Code
x <- 2.0
attr(x, "Mood") <- "Haha!"

To retrieve attributes use attributes():

Code
attributes(x)
$Mood
[1] "Haha!"

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:

Code
y <- c(1, 2, 3)
attr(y, "names") <- c("First", "Second", "Third")
y
 First Second  Third 
     1      2      3 
Code
attributes(y)
$names
[1] "First"  "Second" "Third" 

Now we can call elements by their name:

Code
y["Second"]
Second 
     2 

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:

Code
xx <- factor(c(1,2))
typeof(xx)
[1] "integer"

Fortunately, the standard test functions (is.*()) usually work, so you can use is.factor():

Code
is.factor(xx)
[1] TRUE

Alternatively, you can always inspect the attributes of the object to find out about its class:

Code
attributes(xx)
$levels
[1] "1" "2"

$class
[1] "factor"

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.

2 Factors

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():

Code
x <- c("Female", "Male", "Female")
x <- factor(c("Female", "Male", "Female"))
x
[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:

Code
x <- c("Female", "Male", "Female")
x <- factor(c("Female", "Male", "Female"),
            levels=c("Diverse","Female", "Male"))
x
[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:

Code
x <- c("Female", "Male", "Female")
x <- factor(c("Female", "Male", "Female", "Diverse"),
            levels=c("Female", "Male"))
x
[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:

Code
x <- c("High", "High", "Low", "High")
x <- factor(x,
            levels = c("Low", "Mid", "High"),
            ordered = TRUE)
x
[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:

Code
x <- factor(c("Female", "Male", "Female"),
            levels=c("Male", "Female", "Diverse"))
typeof(x)
[1] "integer"

To check whether an object is a factor we use the function is.factor():

Code
is.factor(x)
[1] TRUE

Not all operations that are defined for integers, however, also work for factors:

Code
x[1] + x[2]
[1] NA

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:

Code
table(x)
x
   Male  Female Diverse 
      1       2       0 

3 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():

Code
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:

Code
df_3
  a b c
1 1 4 7
2 2 5 8
3 3 6 9

The relation to lists becomes obvious if we call typeof(), which returns the underlying data type:

Code
typeof(df_3)
[1] "list"

To test whether an object is a data.frame we use is.data.frame:

Code
is.data.frame(df_3)
[1] TRUE
Code
is.data.frame(l_3)
[1] FALSE

The difference to a list becomes particularly obvious in the different printing behavior of the two:

Code
l_3
$a
[1] 1 2 3

$b
[1] 4 5 6

$c
[1] 7 8 9
Code
df_3
  a b c
1 1 4 7
2 2 5 8
3 3 6 9

A more direct way to create a data.frame is to use the function data.frame():

Code
df_4 <- data.frame(
  "gender" = c(rep("male", 3), rep("female", 2)),
  "height" = c(189, 175, 180, 166, 150)
)
df_4
  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:

Code
df_4["gender"]
  gender
1   male
2   male
3   male
4 female
5 female

The result is another data.frame:

Code
is.data.frame(df_4["gender"])
[1] TRUE

If we want to extract the underlying atomic vector we must use [[:

Code
df_4[["gender"]]
[1] "male"   "male"   "male"   "female" "female"

This is also helpful to inspect the underlying data type:

Code
typeof(df_4[["gender"]])
[1] "character"

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:

Code
names(df_4)
[1] "gender" "height"

Hint: For large data.frames 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:

Code
head(df_4, n = 2)
  gender height
1   male    189
2   male    175

The package dplyr provides the useful function dplyr::glimpse(), which gives a general overview about all columns and their types:

Code
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.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 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.frames and it is the original approach envisioned by the R developers. The tidyverse dialect represents data as tibbles 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 as data.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.

4 Digression: Matrices in R

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):

Code
m_1 <- matrix(11:20, nrow = 5)
m_1
     [,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:

Code
m_1[,1] # First column
[1] 11 12 13 14 15
Code
m_1[1,] # First row
[1] 11 16
Code
m_1[2,2] # Element [2,2]
[1] 17

To test whether an object is a matrix use is.matrix():

Code
is.matrix(m_1)
[1] TRUE

5 Practice

The ObjectTypes2 exercise set in the DataScienceExercises package covers the main concepts from this tutorial:

Code
learnr::run_tutorial(
  name = "ObjectTypes2",
  package = "DataScienceExercises",
  shiny_args = list("launch.browser" = TRUE)
)

See the blog post on DataScienceExercises for installation instructions.

6 Further reading

Citation

BibTeX citation:
@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}
}
For attribution, please cite this work as:
Gräbner-Radkowitsch, Claudius. 2026. “Object Types in R III: Factors and Data Frames.” In Method and Inquiry, edited by Claudius Gräbner-Radkowitsch. June 16. https://euf-methodology-hub.netlify.app/resources/r-object-types-iii-factors-dataframes.