R data structures
There are numerous types of data structures found in programming languages, each with strengths and weaknesses suited to specific tasks. Since R is a programming language used widely for statistical data analysis, the data structures it utilizes were designed with this type of work in mind.
The R data structures used most frequently in machine learning are vectors, factors, lists, arrays, matrices, and data frames. Each is tailored to a specific data management task, which makes it important to understand how they will interact in your R project. In the sections that follow, we will review their similarities and differences.
Vectors
The fundamental R data structure is a vector, which stores an ordered set of values called elements. A vector can contain any number of elements. However, all of a vector’s elements must be of the same type; for instance, a vector cannot contain both numbers and text. To determine the type of vector v
, use the typeof(v)
command. Note that R is a case-sensitive language, which means that lower-case v
and upper-case V
could represent two different vectors. This is also true for R’s built-in functions and keywords, so be sure to always use the correct capitalization when typing R commands or expressions.
Several vector types are commonly used in machine learning: integer
(numbers without decimals), double
(numbers with decimals), character
(text data, also commonly called “string” data), and logical
(TRUE
or FALSE
values). Some R functions will report both integer
and double
vectors as numeric
, while others distinguish between the two; generally, this distinction is unimportant. Vectors of logical values are used often in R, but notice that the TRUE
and FALSE
values must be written in all caps. This is slightly different from some other programming languages.
There are also two special values that are relevant to all vector types: NA
, which indicates a missing value, and NULL
, which is used to indicate the absence of any value. Although these two may seem to be synonymous, they are indeed slightly different. The NA
value is a placeholder for something else and therefore has a length of one, while the NULL
value is truly empty and has a length of zero.
It is tedious to enter large amounts of data by hand, but simple vectors can be created by using the c()
combine function. The vector can also be given a name using the arrow <-
operator. This is R’s assignment operator, used much like the =
assignment operator used in many other programming languages.
R also allows the use of the =
operator for assignment, but it is considered a poor coding style according to commonly accepted style guidelines.
For example, let’s construct a set of vectors containing data on three medical patients. We’ll create a character vector named subject_name
to store the three patient names, a numeric vector named temperature
to store each patient’s body temperature in degrees Fahrenheit, and a logical vector named flu_status
to store each patient’s diagnosis (TRUE
if they have influenza, FALSE
otherwise). As shown in the following code, the three vectors are:
> subject_name <- c("John Doe", "Jane Doe", "Steve Graves")
> temperature <- c(98.1, 98.6, 101.4)
> flu_status <- c(FALSE, FALSE, TRUE)
Values stored in R vectors retain their order. Therefore, data for each patient can be accessed using their position in the set, beginning at 1
, then supplying this number inside square brackets (that is, [
and ]
) following the name of the vector. For instance, to obtain the temperature value for patient Jane Doe, the second patient, simply type:
> temperature[2]
[1] 98.6
R offers a variety of methods to extract data from vectors. A range of values can be obtained using the colon operator. For instance, to obtain the body temperature of the second and third patients, type:
> temperature[2:3]
[1] 98.6 101.4
Items can be excluded by specifying a negative item number. To exclude the second patient’s temperature data, type:
> temperature[-2]
[1] 98.1 101.4
It is also sometimes useful to specify a logical vector indicating whether each item should be included. For example, to include the first two temperature readings but exclude the third, type:
> temperature[c(TRUE, TRUE, FALSE)]
[1] 98.1 98.6
The importance of this type of operation is clearer with the realization that the result of a logical expression like temperature > 100
is a logical vector. This expression returns TRUE
or FALSE
depending on whether the temperature is greater than 100 degrees Fahrenheit, which indicates a fever. Therefore, the following commands will identify the patients exhibiting a fever:
> fever <- temperature > 100
> subject_name[fever]
[1] "Steve Graves"
Alternatively, the logical expression can also be moved inside the brackets, which returns the same result in a single step:
> subject_name[temperature > 100]
[1] "Steve Graves"
As you will see shortly, the vector provides the foundation for many other R data structures and can be combined with programming expressions to complete more complex operations for selecting data and constructing new features. Therefore, knowing the various vector operations is crucial for working with data in R.
Factors
Recall from Chapter 1, Introducing Machine Learning, that nominal features represent a characteristic with categories of values. Although it is possible to use a character vector to store nominal data, R provides a data structure specifically for this task.
A factor is a special type of vector that is solely used for representing categorical or ordinal data. In the medical dataset we are building, we might use a factor to represent the patients’ biological sex and record two categories: male and female.
Why use factors rather than character vectors? One advantage of factors is that the category labels are stored only once. Rather than storing MALE
, MALE
, FEMALE
, the computer may store 1
, 1
, 2
, which can reduce the memory needed to store the values. Additionally, many machine learning algorithms handle nominal and numeric features differently. Coding categorical features as factors allows R to treat the categorical features appropriately.
A factor should not be used for character vectors with values that don’t truly fall into categories. If a vector stores mostly unique values such as names or identification codes like social security numbers, keep it as a character vector.
To create a factor from a character vector, simply apply the factor()
function. For example:
> gender <- factor(c("MALE", "FEMALE", "MALE"))
> gender
[1] MALE FEMALE MALE
Levels: FEMALE MALE
Notice that when the gender
factor was displayed, R printed additional information about its levels. The levels comprise the set of possible categories the factor could take, in this case, MALE
or FEMALE
.
When we create factors, we can add additional levels that may not appear in the original data. Suppose we created another factor for blood type, as shown in the following example:
> blood <- factor(c("O", "AB", "A"),
levels = c("A", "B", "AB", "O"))
> blood
[1] O AB A
Levels: A B AB O
When we defined the blood
factor, we specified an additional vector of four possible blood types using the levels
parameter. As a result, even though our data includes only blood types O, AB, and A, all four types are retained with the blood
factor, as the output shows. Storing the additional level allows for the possibility of adding patients with the other blood type in the future. It also ensures that if we were to create a table of blood types, we would know that type B exists, despite it not being found in our initial data.
The factor data structure also allows us to include information about the order of a nominal feature’s categories, which provides a method for creating ordinal features. For example, suppose we have data on the severity of patient symptoms, coded in increasing order of severity from mild, to moderate, to severe. We indicate the presence of ordinal data by providing the factor’s levels in the desired order, listed ascending from lowest to highest, and setting the ordered
parameter to TRUE
as shown:
> symptoms <- factor(c("SEVERE", "MILD", "MODERATE"),
levels = c("MILD", "MODERATE", "SEVERE"),
ordered = TRUE)
The resulting symptoms
factor now includes information about the requested order. Unlike our prior factors, the levels of this factor are separated by <
symbols to indicate the presence of a sequential order from MILD
to SEVERE
:
> symptoms
[1] SEVERE MILD MODERATE
Levels: MILD < MODERATE < SEVERE
A helpful feature of ordered factors is that logical tests work as you would expect. For instance, we can test whether each patient’s symptoms are more severe than moderate:
> symptoms > "MODERATE"
[1] TRUE FALSE FALSE
Machine learning algorithms capable of modeling ordinal data will expect ordered factors, so be sure to code your data accordingly.
Lists
A list is a data structure, much like a vector, in that it is used for storing an ordered set of elements. However, where a vector requires all its elements to be the same type, a list allows different R data types to be collected. Due to this flexibility, lists are often used to store various types of input and output data and sets of configuration parameters for machine learning models.
To illustrate lists, consider the medical patient dataset we have been constructing, with data for three patients stored in six vectors. If we wanted to display all the data for the first patient, we would need to enter five R commands:
> subject_name[1]
[1] "John Doe"
> temperature[1]
[1] 98.1
> flu_status[1]
[1] FALSE
> gender[1]
[1] MALE
Levels: FEMALE MALE
> blood[1]
[1] O
Levels: A B AB O
> symptoms[1]
[1] SEVERE
Levels: MILD < MODERATE < SEVERE
If we expect to examine the patient’s data again in the future, rather than retyping these commands, a list allows us to group all the values into one object we can use repeatedly.
Similar to creating a vector with c()
, a list is created using the list()
function, as shown in the following example. One notable difference is that when a list is constructed, each component in the sequence should be given a name. The names are not strictly required, but allow the values to be accessed later by name rather than by numbered position and a mess of square brackets. To create a list with named components for the first patient’s values, type the following:
> subject1 <- list(fullname = subject_name[1],
temperature = temperature[1],
flu_status = flu_status[1],
gender = gender[1],
blood = blood[1],
symptoms = symptoms[1])
This patient’s data is now collected in the subject1
list:
> subject1
$fullname
[1] "John Doe"
$temperature
[1] 98.1
$flu_status
[1] FALSE
$gender
[1] MALE
Levels: FEMALE MALE
$blood
[1] O
Levels: A B AB O
$symptoms
[1] SEVERE
Levels: MILD < MODERATE < SEVERE
Note that the values are labeled with the names we specified in the preceding command. As a list retains order like a vector, its components can be accessed using numeric positions, as shown here for the temperature
value:
> subject1[2]
$temperature
[1] 98.1
The result of using vector-style operators on a list object is another list object, which is a subset of the original list. For example, the preceding code returned a list with a single temperature
component. To instead return a single list item in its native data type, use double brackets ([[
and ]]
) when selecting the list component. For example, the following command returns a numeric vector of length 1:
> subject1[[2]]
[1] 98.1
For clarity, it is often better to access list components by name, by appending a $
and the component name to the list name as follows:
> subject1$temperature
[1] 98.1
Like the double-bracket notation, this returns the list component in its native data type (in this case, a numeric vector of length 1).
Accessing the value by name also ensures that the correct item is retrieved even if the order of the list elements is changed later.
It is possible to obtain several list items by specifying a vector of names. The following returns a subset of the subject1
list, which contains only the temperature
and flu_status
components:
> subject1[c("temperature", "flu_status")]
$temperature
[1] 98.1
$flu_status
[1] FALSE
Entire datasets could be constructed using lists, and lists of lists. For example, you might consider creating a subject2
and subject3
list and grouping these into a list object named pt_data
. However, constructing a dataset in this way is common enough that R provides a specialized data structure specifically for this task.
Data frames
By far the most important R data structure for machine learning is the data frame, a structure analogous to a spreadsheet or database in that it has both rows and columns of data. In R terms, a data frame can be understood as a list of vectors or factors, each having exactly the same number of values. Because the data frame is literally a list of vector-type objects, it combines aspects of both vectors and lists.
Let’s create a data frame for our patient dataset. Using the patient data vectors we created previously, the data.frame()
function combines them into a data frame:
> pt_data <- data.frame(subject_name, temperature,
flu_status, gender, blood, symptoms)
When displaying the pt_data
data frame, we see that the structure is quite different from the data structures we’ve worked with previously:
> pt_data
subject_name temperature flu_status gender blood symptoms
1 John Doe 98.1 FALSE MALE O SEVERE
2 Jane Doe 98.6 FALSE FEMALE AB MILD
3 Steve Graves 101.4 TRUE MALE A MODERATE
Compared to one-dimensional vectors, factors, and lists, a data frame has two dimensions and is displayed in a tabular format. Our data frame has one row for each patient and one column for each vector of patient measurements. In machine learning terms, the data frame’s rows are the examples, and the columns are the features or attributes.
To extract entire columns (vectors) of data, we can take advantage of the fact that a data frame is simply a list of vectors. Like lists, the most direct way to extract a single element is by referring to it by name. For example, to obtain the subject_name
vector, type:
> pt_data$subject_name
[1] "John Doe" "Jane Doe" "Steve Graves"
Like lists, a vector of names can be used to extract multiple columns from a data frame:
> pt_data[c("temperature", "flu_status")]
temperature flu_status
1 98.1 FALSE
2 98.6 FALSE
3 101.4 TRUE
When we request data frame columns by name, the result is a data frame containing all rows of data for the specified columns. The command pt_data[2:3]
will also extract the temperature
and flu_status
columns. However, referring to the columns by name results in clear and easy-to-maintain R code, which will not break if the data frame is later reordered.
To extract specific values from the data frame, methods like those for accessing values in vectors are used. However, there is an important distinction—because the data frame is two-dimensional, both the desired rows and columns must be specified. Rows are specified first, followed by a comma, followed by the columns in a format like this: [rows, columns]
. As with vectors, rows and columns are counted beginning at one.
For instance, to extract the value in the first row and second column of the patient data frame, use the following command:
> pt_data[1, 2]
[1] 98.1
If you would like more than a single row or column of data, specify vectors indicating the desired rows and columns. The following statement will pull data from the first and third rows and the second and fourth columns:
> pt_data[c(1, 3), c(2, 4)]
temperature gender
1 98.1 MALE
3 101.4 MALE
To refer to every row or every column, simply leave the row or column portion blank. For example, to extract all rows of the first column:
> pt_data[, 1]
[1] "John Doe" "Jane Doe" "Steve Graves"
To extract all columns for the first row:
> pt_data[1, ]
subject_name temperature flu_status gender blood symptoms
1 John Doe 98.1 FALSE MALE O SEVERE
And to extract everything:
> pt_data[ , ]
subject_name temperature flu_status gender blood symptoms
1 John Doe 98.1 FALSE MALE O SEVERE
2 Jane Doe 98.6 FALSE FEMALE AB MILD
3 Steve Graves 101.4 TRUE MALE A MODERATE
Of course, columns are better accessed by name rather than position, and negative signs can be used to exclude rows or columns of data. Therefore, the output of the command:
> pt_data[c(1, 3), c("temperature", "gender")]
temperature gender
1 98.1 MALE
3 101.4 MALE
is equivalent to:
> pt_data[-2, c(-1, -3, -5, -6)]
temperature gender
1 98.1 MALE
3 101.4 MALE
We often need to create new columns in data frames—perhaps, for instance, as a function of existing columns. For example, we may need to convert the Fahrenheit temperature readings in the patient data frame into the Celsius scale. To do this, we simply use the assignment operator to assign the result of the conversion calculation to a new column name as follows:
> pt_data$temp_c <- (pt_data$temperature - 32) * (5 / 9)
To confirm the calculation worked, let’s compare the new Celsius-based temp_c
column to the previous Fahrenheit-scale temperature
column:
> pt_data[c("temperature", "temp_c")]
temperature temp_c
1 98.1 36.72222
2 98.6 37.00000
3 101.4 38.55556
Seeing these side by side, we can confirm that the calculation has worked correctly.
As these types of operations are crucial for much of the work we will do in upcoming chapters, it is important to become very familiar with data frames. You might try practicing similar operations with the patient dataset, or even better, use data from one of your own projects—the functions to load your own data files into R will be described later in this chapter.
Matrices and arrays
In addition to data frames, R provides other structures that store values in tabular form. A matrix is a data structure that represents a two-dimensional table with rows and columns of data. Like vectors, R matrices can contain only one type of data, although they are most often used for mathematical operations and therefore typically store only numbers.
To create a matrix, simply supply a vector of data to the matrix()
function, along with a parameter specifying the number of rows (nrow
) or number of columns (ncol
). For example, to create a 2x2 matrix storing the numbers one to four, we can use the nrow
parameter to request the data be divided into two rows:
> m <- matrix(c(1, 2, 3, 4), nrow = 2)
> m
[,1] [,2]
[1,] 1 3
[2,] 2 4
This is equivalent to the matrix produced using ncol = 2
:
> m <- matrix(c(1, 2, 3, 4), ncol = 2)
> m
[,1] [,2]
[1,] 1 3
[2,] 2 4
You will notice that R loaded the first column of the matrix first before loading the second column. This is called column-major order, which is R’s default method for loading matrices.
To override this default setting and load a matrix by rows, set the parameter byrow = TRUE
when creating the matrix.
To illustrate this further, let’s see what happens if we add more values to the matrix. With six values, requesting two rows creates a matrix with three columns:
> m <- matrix(c(1, 2, 3, 4, 5, 6), nrow = 2)
> m
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
Requesting two columns creates a matrix with three rows:
> m <- matrix(c(1, 2, 3, 4, 5, 6), ncol = 2)
> m
[,1] [,2]
[1,] 1 4
[2,] 2 5
[3,] 3 6
As with data frames, values in matrices can be extracted using [row, column]
notation. For instance, m[1, 1]
will return the value 1
while m[3, 2]
will extract 6
from the m
matrix. Additionally, entire rows or columns can be requested:
> m[1, ]
[1] 1 4
> m[, 1]
[1] 1 2 3
Closely related to the matrix structure is the array, which is a multidimensional table of data. Where a matrix has rows and columns of values, an array has rows, columns, and one or more additional layers of values. Although we will occasionally use matrices in later chapters, the use of arrays is unnecessary within the scope of this book.