Real data rarely arrives in the form we want it in.
In the previous labs, we focused primarily on accessing data and
understanding various ways R can store that data. In Lab 1,
we also reviewed how data frames are organized into rows and columns and
how individual observations and variables can be accessed. In Lab 2, we
used logical conditions to identify and subset observations that meet
particular criteria.
In this lab, we will begin modifying data frames or making new ones.
When we manipulate data in R, we can either overwrite an
existing object with the result or give the result a new name so that
both versions remain available.
For example, consider the College Scorecard data we have used previously:
# load in colleges data
colleges = read.csv("https://nfriedrichsen.github.io/data/Colleges2019.csv")
Suppose we only wanted to work with colleges in Iowa. We could
overwrite the colleges data with this smaller data set:
# remove all non-iowa colleges from the data set
colleges = subset(colleges, State == "IA")
After running this code, colleges contains only the Iowa
colleges and the original version with different states is no longer
stored in our R Environment. If I want to go back to
analyze a different subset, I have to reload the original
colleges data set above. Having to constantly reload the
same data is not efficient; it slows down our knitting process by making
the computer do more work and, with larger data sets, can make things
take considerably longer.
Instead, we could give the result a new name:
# reload the original data
colleges = read.csv("https://nfriedrichsen.github.io/data/Colleges2019.csv")
# make the new subset its own data frame
iowa_colleges = subset(colleges, State == "IA")
Now colleges still contains the original data, while
iowa_colleges contains our modified version.
There is a trade-off here; every new object we create takes up some memory, so we wouldn’t necessarily want to make endless copies of a huge data set. Neither approach is always correct. The important consideration we need to make is whether the original data may still be useful later.
Remember that changing an object in R does not change
the raw data file from which it was imported. If we make a mistake, we
can rerun our code from the beginning and import the original data
again, which is one reason that making reproducible code (thus
documenting our operations) is important.
For this lab, we will continue working with the Happy Planet data used in the previous labs:
HappyPlanet = read.csv("https://nfriedrichsen.github.io/data/HappyPlanet.csv")
Recall that we introduced three important types of vectors in the first lab:
x = c(1,2,3)x = c("A","B","C")x = c(TRUE, FALSE, TRUE)Many functions require their inputs to be of a certain type, but that
is not always how data is stored, or sometimes it is converted to
another type when we import it into R. Fortunately, data can be
coerced into another type using the as. family of
functions:
## A character vector where the text strings are numbers
x <- c("1","12","123")
typeof(x)
## [1] "character"
## Coerce 'x' to a numeric vector
x <- as.numeric(x)
x
## [1] 1 12 123
typeof(x)
## [1] "double"
Notice that we overwrote x in this example. After the
assignment is performed, the original character version of
x is no longer stored in the Environment.
Question 1: (Sort of review) Check the type of the
variable Region in HappyPlanet. Then coerce
Region into a character variable and overwrite the original
Region variable with the result. Use typeof()
to verify the change.
Categorical variables are often stored as either character variables
or factors in R. A factor represents a
categorical variable using a set of possible categorical labels known as
levels.
Factors are particularly useful when the categories have a meaningful
ordering that we want R to recognize. Consider the
following vector containing different months:
## A vector containing different months
mons <- c("March","April","January","November","January", "September",
"October","September","November","August","January","November",
"November","February","May","August","July","December",
"August","August","September","November","February","April")
## Convert it to a factor
mons_unordered = factor(mons)
By default, the factor levels are arranged alphabetically:
levels(mons_unordered)
## [1] "April" "August" "December" "February" "January" "July"
## [7] "March" "May" "November" "October" "September"
barplot(table(mons_unordered))
However, alphabetical ordering does not make much sense for months. (This graph is bad, don’t make graphs like this). We can specify the order of the levels manually:
mons_ordered = factor(mons,
levels = c("January","February","March","April",
"May","June","July","August","September",
"October","November","December"),
ordered = TRUE)
levels(mons_ordered)
## [1] "January" "February" "March" "April" "May" "June"
## [7] "July" "August" "September" "October" "November" "December"
barplot(table(mons_ordered))
Question 2: The Ames Housing data contain information from the Ames Assessor’s Office used in computing assessed values for individual residential properties sold in Ames, IA from 2006 to 2010.
A more detailed description of variables can be found at this link.
ames_housing = read.csv("https://nfriedrichsen.github.io/data/AmesHousing.csv")
A. Check the type of the variable MS.SubClass and
compare it with the description of this variable given in the
information link above. Based upon your assessment, explain whether this
variable should be coerced to a different type.
B. The variable Exter.Cond describes the exterior
condition of a house using the categories Po,
Fa, TA, Gd, and Ex,
corresponding to Poor, Fair, Typical/Average, Good, and
Excellent.
Create an ordered factor from Exter.Cond whose
levels run from Poor to Excellent. Store the result as an object named
cond_ordered. Use levels() to verify the
ordering and use barplot() and table() to
construct a bar chart of the resulting factor.
Recall from Lab 1 that data frames are composed of vectors of the
same length. We can use the data.frame() function to
combine vectors into a data frame.
Suppose we recorded the temperature and reaction time for three trials of a chemistry experiment:
temperature = c(20, 40, 60)
reaction_time = c(84.2, 43.7, 29.1)
experiment = data.frame(temperature, reaction_time)
experiment
## temperature reaction_time
## 1 20 84.2
## 2 40 43.7
## 3 60 29.1
typeof(experiment)
## [1] "list"
Each vector becomes a variable (column) in the resulting data frame, and corresponding positions in the vectors become observations (rows). Notice that a data frame is stored as a list of column vectors (variables) in R.
Another way to combine objects is the cbind() function.
The c in cbind() stands for
column. For example:
temperature = c(20, 40, 60)
reaction_time = c(84.2, 43.7, 29.1)
experiment = cbind(temperature, reaction_time)
experiment
## temperature reaction_time
## [1,] 20 84.2
## [2,] 40 43.7
## [3,] 60 29.1
typeof(experiment)
## [1] "double"
Both examples place temperature and
reaction_time beside one another as columns. However,
without directly invoking the data.frame function,
R treats this resulting object as a mathematical matrix.
Although the matrix has named columns, it is not stored as a list of
variable vectors like a data frame, and so the following operation which
otherwise should work, does not.
experiment$reaction_time
## Error in `experiment$reaction_time`:
## ! $ operator is invalid for atomic vectors
The cbind() operation is generally useful when we
already have an existing data frame, and we want to add information for
each observation corresponding to a new variable. For example, suppose
the concentration used in each experimental trial was stored separately
in a different vector:
concentration = c(.10, .15, .20)
experiment = data.frame(temperature, reaction_time) # create data frame
experiment = cbind(experiment, concentration)
experiment # add concentration column
## temperature reaction_time concentration
## 1 20 84.2 0.10
## 2 40 43.7 0.15
## 3 60 29.1 0.20
When using cbind(), R assumes that the
observations are already in the correct order. Therefore, you should
only use cbind() when you know that the rows correspond to
the same observations.
To summarize, data.frame() is primarily used to
construct a new data frame, while cbind()
describes the operation of adding columns together. If you are using
cbind() to add columns to a data frame, then the use of
data.frame(df, newvar) vs cbind(df, newvar)
are generally interchangeable.
We can similarly use rbind() to combine objects by
rows.
Sometimes it can be useful to aggregate several data frames with the
same structure into one larger data frame. For example, we might want to
combine results from multiple experiments or aggregate several years of
data from the same source. The rbind() function will append
the rows of one or more data frames to an initial data frame, provided
their variables are compatible:
df1 <- data.frame(Year = 2024, val = rnorm(3))
df2 <- data.frame(Year = 2025, val = rnorm(3, mean = 10))
rbind(df1, df2)
## Year val
## 1 2024 -0.06919795
## 2 2024 -0.16191750
## 3 2024 -1.11008560
## 4 2025 8.26355472
## 5 2025 10.14050837
## 6 2025 10.61355374
We can also bind an arbitrary number of data frames:
df3 <- data.frame(Year = 2026, val = rnorm(3, mean = -5))
rbind(df1, df2, df3)
## Year val
## 1 2024 -0.06919795
## 2 2024 -0.16191750
## 3 2024 -1.11008560
## 4 2025 8.26355472
## 5 2025 10.14050837
## 6 2025 10.61355374
## 7 2026 -5.61068190
## 8 2026 -4.09419745
## 9 2026 -5.28988243
The distinction between these last two operations is:
rbind() adds observations (rows).cbind() adds variables (columns).For rbind(), the data frames should use the same
variables. For cbind(), the columns must all have the
observations (rows) in the same order; otherwise your observations will
have their variable values jumbled around.
Question 3:
The colleges data set can be loaded using:
colleges <- read.csv("https://nfriedrichsen.github.io/data/Colleges2019.csv")
A. Find the rows corresponding to Iowa State University and Grinnell College.
B. Make two data frames consisting of each row of data respectively.
C. Use rbind() to combine these into a new data
frame and print the results.
Once we have a data frame, we will frequently want to construct new variables using information already contained in the data.
We have previously used $ to access an existing
variable:
HappyPlanet$Happiness
## [1] 5.5 5.6 4.3 7.1 5.0 7.9 7.8 5.3 5.3 5.8 7.6 6.6 3.0 6.1 6.5 5.9 4.7 7.6
## [19] 5.5 3.6 5.9 2.9 4.9 3.9 8.0 4.0 5.4 6.3 6.7 7.3 3.6 3.9 8.5 6.4 6.7 7.2
## [37] 6.9 8.1 5.7 7.6 6.4 6.7 6.7 5.6 4.0 8.0 7.1 4.3 7.2 4.7 6.8 7.4 4.0 6.5
## [55] 5.2 7.0 7.2 5.7 7.8 5.5 5.7 5.6 5.4 8.1 7.1 6.9 6.7 6.8 6.0 6.1 3.7 6.3
## [73] 6.7 5.0 6.2 5.4 4.7 5.8 7.7 5.5 3.7 4.4 6.6 3.8 7.1 5.0 7.7 5.7 5.7 5.6
## [91] 3.8 4.5 5.3 7.7 7.8 7.1 3.8 4.8 8.1 5.6 5.0 7.8 6.9 5.9 5.5 6.5 5.9 5.9
## [109] 5.9 4.2 7.7 4.5 6.0 3.6 7.1 6.1 7.0 5.0 7.6 5.4 4.5 7.9 7.7 5.9 5.1 2.4
## [127] 6.3 2.6 6.7 5.9 5.5 4.5 5.3 7.2 7.4 7.9 6.8 6.0 6.9 6.5 5.2 4.3 2.8
We can also use $ and assignment to create a new
variable. For example, the following logical condition produces
one value for every country in the Happy Planet data:
HappyPlanet$Happiness > 6
## [1] FALSE FALSE FALSE TRUE FALSE TRUE TRUE FALSE FALSE FALSE TRUE TRUE
## [13] FALSE TRUE TRUE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
## [25] TRUE FALSE FALSE TRUE TRUE TRUE FALSE FALSE TRUE TRUE TRUE TRUE
## [37] TRUE TRUE FALSE TRUE TRUE TRUE TRUE FALSE FALSE TRUE TRUE FALSE
## [49] TRUE FALSE TRUE TRUE FALSE TRUE FALSE TRUE TRUE FALSE TRUE FALSE
## [61] FALSE FALSE FALSE TRUE TRUE TRUE TRUE TRUE FALSE TRUE FALSE TRUE
## [73] TRUE FALSE TRUE FALSE FALSE FALSE TRUE FALSE FALSE FALSE TRUE FALSE
## [85] TRUE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE TRUE TRUE TRUE
## [97] FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE TRUE FALSE FALSE
## [109] FALSE FALSE TRUE FALSE FALSE FALSE TRUE TRUE TRUE FALSE TRUE FALSE
## [121] FALSE TRUE TRUE FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE
## [133] FALSE TRUE TRUE TRUE TRUE FALSE TRUE TRUE FALSE FALSE FALSE
Since the resulting logical vector has one value for every row, we can store it as a new variable in the data frame:
HappyPlanet$HighHappiness = HappyPlanet$Happiness > 6
The data frame now contains an additional column:
colnames(HappyPlanet)
## [1] "Country" "Region" "Happiness" "LifeExpectancy"
## [5] "Footprint" "HLY" "HPI" "HPIRank"
## [9] "GDPperCapita" "HDI" "Population" "HighHappiness"
table(HappyPlanet$HighHappiness)
##
## FALSE TRUE
## 77 66
Notice that this uses the same logical conditions that we studied in Lab 2. Instead of using the logical vector only to count or subset observations, we have stored it as a new variable.
We can create numerical variables in the same way. For example, suppose we wanted life expectancy measured in approximate months instead of years:
HappyPlanet$LifeExpectancyMonths = 12 * HappyPlanet$LifeExpectancy
In both cases, the expression on the right side of =
produces a vector containing one value for each observation. That vector
is then stored as a new column of the data frame.
Question 4:
A. Create a new logical variable in HappyPlanet
named HighLifeExpectancy that identifies countries whose
life expectancy is at least 75 years.
B. Use table() to summarize the new
variable.
C. How many countries have both
HighLifeExpectancy == TRUE and a Happiness score greater
than 6?
Creating a new variable and modifying an existing variable use very similar syntax. The difference is whether the name on the left side of the assignment already exists in the data frame.
## Create a new variable
my_data$new_variable = ...
## Replace an existing variable
my_data$existing_variable = ...
We already modified a variable in Question 1 when we replaced
Region with a character version of itself. In that
situation, we decided that we no longer needed the original numerical
representation of Region.
We can also modify only particular elements of a variable. Recall that square brackets allow us to access particular positions in a vector:
x = c(10, 20, 30, 40)
x[2]
## [1] 20
Assignment can be used with the same indexing syntax:
x[2] = 100
x
## [1] 10 100 30 40
Logical vectors can also be used to identify the positions that should be changed:
x[x > 50] = 50 # replace values 'larger than 50' with 50
x
## [1] 10 50 30 40
Thus, the logical conditions from Lab 2 can be used not only to find observations, but also to specify which values should be modified.
When deciding whether to overwrite an existing variable or create a new one, consider whether you may need the original information later. Changing how a variable is stored, such as coercing a variable from character to numeric, may provide little reason to preserve the old version. A transformation that changes the values themselves may be more useful to store as a new variable.
Question 5:
Using the ames_housing data:
Part A: Find the number of homes in this data set
with missing values for the variable Garage.Type. Hint:
is.na() and sum functions
Part B: Create a subset of ames_housing
named ames_missing that contains homes with a missing value
for the variable Garage.Type. What is the average
Garage.Area of these homes?
Part C: Create a subset of ames_housing
named ames_complete that contains no missing values for the
variable Garage.Type. What is the average
Garage.Area of these homes? (Hint: You should not get NA
for the value)
Part D: A simple plot of the
Garage.Area variable shows it is right-skewed. Many methods
we’ve seen in 209 required variables to have a Normal distribution. A
common fix for this, is that if we have a right-skewed distribution,
some times applying the log-transform (taking the log of a variable)
results in a transformed variable that looks Normal.
Using ames_complete, create a new variable named
LogGarageArea containing the natural logarithm of
Garage.Area. Use the log() function to perform
this. Verify that the original Garage.Area still exists
within this data set. Then run the following line of code exactly as
written and describe the shape of the distribution of
log-Garage-Area.
hist(ames_complete$LogGarageArea)