Week 2
Factors + Packages

PSYC 70510 · Statistical Methods in Psych Lab I · Section 53950
Lab Instructor: Kaile Smith

This page is meant to stay open while you work in RStudio. Add the code to a Week 2 script, run one block at a time, and compare your Console output with the output shown here.

1 What we will do today

  1. Review vectors and common data types.
  2. Create factors with levels and labels.
  3. Set the order and reference level of a factor.
  4. Install and load R packages.
  5. Use psych to calculate descriptive statistics.
  6. Filter data with comparison operators and multiple conditions.
  7. Link tidyverse steps with the pipe.
  8. Use the remaining class time for group work, questions, and troubleshooting.

2 Create the Week 2 script

Open the PSYC-70510 Project, then create a new R script:

  1. Choose File → New File → R Script.
  2. Choose File → Save As.
  3. Save the file as StatsLab_Week2_FactorsPackages.R inside the Project’s scripts folder.

Add a title comment:

# Lab 2: Factors and Packages

3 Review: one vector has one common type

Week 1 introduced numeric, character, and logical values.

Type Examples Used for
numeric 7, 3.5, -2 calculations
character "male", "college" text and names
logical TRUE, FALSE conditions

When logical values appear with numbers, R converts TRUE to 1 and FALSE to 0:

c(10, 7, 9, 3, TRUE, FALSE)
[1] 10 7 9 3 1 0

When text appears in the same vector, R converts every value to character text:

c("hi", "hey", 7, TRUE)
[1] "hi"   "hey"  "7"    "TRUE"

This matters because a data-frame column is a vector. Every value in one column follows one common data type.

4 What is a factor?

A factor represents a categorical variable with a known set of possible values. Those possible values are called levels.

Character vector Factor
stores text stores categories
has no declared set of possible values records a fixed set of levels
has no category order can record a level order

Create a character vector containing the days of the week:

days_of_week <- c(
  "Sunday", "Monday", "Tuesday", "Wednesday",
  "Thursday", "Friday", "Saturday"
)

class(days_of_week)
[1] "character"

4.1 Convert the vector with factor()

factor(days_of_week)
[1] Sunday Monday Tuesday Wednesday Thursday Friday Saturday
Levels: Friday Monday Saturday Sunday Thursday Tuesday Wednesday

R created and printed a factor, but the original object was not changed. Because the result was not assigned with <-, days_of_week is still a character vector:

is.factor(days_of_week)
[1] FALSE

is.factor() is a quick check: it returns TRUE for a factor and FALSE for another type of object.

To keep the converted factor, assign the result of factor() to an object. By default, R arranges the factor levels alphabetically:

Use levels() to inspect the stored level order:

days_default <- factor(days_of_week)
is.factor(days_default)
levels(days_default)
[1] TRUE
[1] "Friday" "Monday" "Saturday" "Sunday"
[5] "Thursday" "Tuesday" "Wednesday"

4.2 Supply the levels explicitly

The first argument to factor() is the vector to convert. The levels argument states the order of the possible values:

days_factor <- factor(
  days_of_week,
  levels = days_of_week
)

levels(days_factor)
is.factor(days_factor)
[1] "Sunday" "Monday" "Tuesday" "Wednesday"
[5] "Thursday" "Friday" "Saturday"
[1] TRUE

The levels now follow the order supplied in days_of_week.

4.3 Mark a factor as ordered

Some categories have a meaningful progression. Add ordered = TRUE when comparisons such as earlier/later or low/medium/high make sense:

days_factor <- factor(
  days_of_week,
  levels = days_of_week,
  ordered = TRUE
)

class(days_factor)
is.ordered(days_factor)
[1] "ordered" "factor"
[1] TRUE

Setting the level order and declaring an ordered factor are related but separate choices. Level order controls how categories are stored and displayed; ordered = TRUE additionally tells R that the categories have a meaningful ranking.

4.4 Replace numeric codes with labels

Sometimes categories arrive as numeric codes. Here, the values 1 through 7 stand for days:

day_codes <- 1:7
day_codes
[1] 1 2 3 4 5 6 7

Use levels to identify the original codes and labels to supply the readable names:

labeled_days <- factor(
  day_codes,
  levels = 1:7,
  labels = days_of_week,
  ordered = TRUE
)

labeled_days
[1] Sunday Monday Tuesday Wednesday Thursday Friday Saturday
7 Levels: Sunday < Monday < Tuesday < Wednesday < ... < Saturday

Because the call begins with labeled_days <-, the factor is already saved as a new object. To use another name, change the name on the left side of <- when you create it.

4.5 Read the function documentation

If you get stuck or want more information about factors, open the Help page with either form:

?factor
help("factor")

The Help page lists the function’s arguments, which arguments are required, and examples of valid calls.

5 Factors in mtcars

mtcars is a dataset included with R. In Week 1, we used a CSV copy so you could practice importing a file. Here, we use the built-in version directly.

Assign it to an object so the original built-in dataset remains unchanged:

cars <- mtcars

Open the data viewer and inspect the structure:

View(cars)
str(cars)

View() opens a spreadsheet-style viewer in RStudio. str() prints the data-frame structure in the Console.

5.1 Inspect a possible categorical variable

The cyl column records each car’s number of cylinders:

unique(cars$cyl)
table(cars$cyl)
[1] 6 4 8

 4  6  8
11  7 14

unique() returns the distinct values. table() counts how often each value appears.

5.2 Turn cylinder values into labeled groups

The values 4, 6, and 8 identify categories of cars. Convert them to a factor and give each category a readable label:

cars$cyl_factor <- factor(
  cars$cyl,
  levels = c(4, 6, 8),
  labels = c("4 cylinder", "6 cylinder", "8 cylinder")
)

table(cars$cyl_factor)
4 cylinder 6 cylinder 8 cylinder
        11          7         14

Inspect the structure again after adding the factor column:

str(cars)

In the output, cyl_factor is identified as a factor with three levels. This confirms that the new column is stored differently from the original numeric cyl column.

The original numeric cyl column is still available for numeric operations. The new cyl_factor column tells R to treat the values as three groups.

You can also create the factor without supplying new labels:

cars$cyl_factor2 <- factor(cars$cyl)
levels(cars$cyl_factor2)
[1] "4" "6" "8"

Both factor columns contain the same three categories. The first uses labels that are easier to read in output.

The original numeric column can be averaged:

mean(cars$cyl)
[1] 6.1875

For most research questions, cylinders are more useful as groups. For example, we may compare mean mpg across 4-, 6-, and 8-cylinder cars.

6 Set a reference level with relevel()

Many statistical models compare every factor level with one reference level. Check the current order:

levels(cars$cyl_factor)
[1] "4 cylinder" "6 cylinder" "8 cylinder"

Set "8 cylinder" as the reference:

cars$cyl_factor <- relevel(
  cars$cyl_factor,
  ref = "8 cylinder"
)

levels(cars$cyl_factor)
[1] "8 cylinder" "4 cylinder" "6 cylinder"

6.1 See the reference level in a model

To see why a reference level matters, we will briefly fit a simple linear model. You do not need to interpret the full model yet.

The function lm() fits a linear model. Inside it, mpg ~ cyl_factor is a formula:

Here, the model compares mean mpg across the cylinder groups.

cylinder_model <- lm(
  mpg ~ cyl_factor,
  data = cars
)

coef(cylinder_model)
                  (Intercept) cyl_factor4 cylinder cyl_factor6 cylinder
                    15.100000             11.563636              4.642857

The intercept is the mean mpg for the 8-cylinder reference group. The other coefficients describe how the 4- and 6-cylinder means differ from that reference.

Use summary(cylinder_model) when you need the full model output:

summary(cylinder_model)

7 What is an R package?

Base R provides the functions available when R first opens. A package adds functions, datasets, or documentation created for a particular purpose.

This week we will use three packages:

Package What we use it for
psych descriptive statistics
tidyverse filtering, selecting, grouping, and piping
datarium the jobsatisfaction practice dataset

The psych package contains tools developed for psychological research, including the descriptive-statistics functions used here.

tidyverse is a collection of related packages. The functions filter(), select(), group_by(), and summarize() come from its dplyr package. Running library(tidyverse) loads dplyr, so a separate library(dplyr) call is not required for this lesson.

7.1 Install once

Installation downloads a package onto the computer. Run these commands in the Console if the packages are not already installed:

install.packages("psych")
install.packages("tidyverse")
install.packages("datarium")

Package installation may print many messages while dependencies are installed. Those messages are expected. Do not place install.packages() in a script that will run every session.

7.2 Load each R session

Loading attaches an installed package to the current R session. Put these lines near the top of the Week 2 script:

library(psych)
library(tidyverse)
library(datarium)

Run library() again after restarting RStudio. Installing again is usually unnecessary.

7.3 Find package documentation

help(package = "psych")
help("describe", package = "psych")

The first line opens the package index. The second opens the Help page for one function.

7.4 Use package::function()

Two colons identify exactly which package should supply a function:

psych::describe(cars$mpg)

Read this as: use describe() from the psych package.

This notation is helpful when two loaded packages contain functions with the same name. For example, tidyverse startup messages may say that dplyr::filter() masks stats::filter(). An unqualified call to filter() will then use the version from dplyr.

8 Descriptive statistics with psych

Use describe() for a compact set of descriptive statistics:

psych::describe(cars$mpg)
   vars  n  mean   sd median trimmed  mad  min  max range skew kurtosis   se
X1    1 32 20.09 6.03   19.2   19.95 5.41 10.4 33.9  23.5 0.61    -0.37 1.07

The output includes sample size, mean, standard deviation, median, minimum, maximum, range, skew, kurtosis, and standard error.

8.1 Describe a variable by one group

Use describeBy() when descriptive statistics should be calculated separately for each group:

psych::describeBy(
  cars$mpg,
  group = cars$cyl_factor,
  mat = TRUE
)
       group1  n  mean   sd median  min  max
1  4 cylinder 11 26.66 4.51   26.0 21.4 33.9
2  6 cylinder  7 19.74 1.45   19.7 17.8 21.4
3  8 cylinder 14 15.10 2.56   15.2 10.4 19.2

The first argument is the numeric variable to summarize. The group argument identifies the grouping factor. mat = TRUE returns one rectangular table.

8.2 Describe a variable by two groups

Use list() to supply more than one grouping variable:

psych::describeBy(
  cars$mpg,
  group = list(
    cylinders = cars$cyl_factor,
    gears = cars$gear
  ),
  mat = TRUE
)

list() keeps the grouping variables together. The output contains one row for each observed combination of cylinder group and gear value.

9 Filter rows with filter()

filter() keeps rows that satisfy a condition. The first argument is the data frame. The remaining arguments are conditions.

Keep cars with six-cylinder engines:

six_cyl_cars <- filter(cars, cyl == 6)
View(six_cyl_cars)

Read filter(cars, cyl == 6) as: within cars, check the cyl value in every row and keep the rows where it equals 6. The comparison cyl == 6 produces TRUE for rows to keep and FALSE for rows to remove.

Use the double equals sign == to ask whether two values are equal. A single equals sign is used for named function arguments, such as group = cars$cyl_factor, rather than for this comparison.

9.1 Comparison operators

Operator Meaning
== equal to
!= not equal to
> greater than
< less than
>= greater than or equal to
<= less than or equal to

Type and run these examples:

not_six_cyl_cars <- filter(cars, cyl != 6)
over_20_mpg_cars <- filter(cars, mpg > 20)
at_most_20_mpg_cars <- filter(cars, mpg <= 20)

Use range() to check the smallest and largest values in a numeric vector:

range(over_20_mpg_cars$mpg)
[1] 21.0 33.9

9.2 Require two conditions

Separate conditions with commas when every condition must be true:

six_cyl_20_mpg <- filter(
  cars,
  cyl == 6,
  mpg > 20
)

rownames(six_cyl_20_mpg)
[1] "Mazda RX4" "Mazda RX4 Wag" "Hornet 4 Drive"

A comma inside filter() works like the AND operator &. This equivalent form returns the same rows:

filter(cars, cyl == 6 & mpg > 20)

9.3 Accept either of several values

Join two comparisons with the OR operator | when either condition can be true:

filter(cars, gear == 4 | gear == 5)

Here, a row is kept when gear equals 4 or when gear equals 5.

The operator %in% provides a shorter way to check whether a value appears in a vector. This code returns the same rows:

four_or_five_gears <- filter(
  cars,
  gear %in% c(4, 5)
)

four_or_five_gears

Use %in% when one variable can match several named categories. It is usually easier to read than repeating the column name.

The pipe %>% takes the object or result on its left and supplies it as the first input to the function on the next line. This lets you write a sequence of steps from top to bottom.

cars %>%
  filter(mpg > 20)

Read the code from top to bottom: begin with cars, then pass it into filter() and keep rows where mpg is greater than 20.

The code above does the same thing as filter(cars, mpg > 20). The data-frame name moves outside filter() because the pipe supplies cars as the first argument.

six_cyl_20_mpg <- cars %>%
  filter(cyl == 6) %>%
  filter(mpg > 20)

The first filter() returns six-cylinder cars. The second receives those rows and keeps the cars above 20 mpg.

The same result can be written with both conditions in one call:

six_cyl_20_mpg <- cars %>%
  filter(cyl == 6, mpg > 20)

11 Select, group, and summarize

select() keeps columns. This example keeps wt, mpg, and cyl:

selected_cars <- cars %>%
  select(wt, mpg, cyl)

group_by() records the groups to use in the next calculation. summarize() produces one summary row per group:

cylinder_weights <- cars %>%
  select(wt, mpg, cyl) %>%       # keep the three columns we need
  group_by(cyl) %>%              # make one group for each cyl value
  summarize(
    mean_wt = mean(wt),          # calculate mean wt within each group
    .groups = "drop"             # remove the grouping when finished
  )

cylinder_weights
# A tibble: 3 × 2
    cyl mean_wt
  <dbl>   <dbl>
1     4    2.29
2     6    3.12
3     8    4.00

group_by(cyl) temporarily marks the rows as belonging to cylinder groups. .groups = "drop" removes that grouping instruction after the means are calculated, so cylinder_weights behaves like a regular, ungrouped data frame. It does not remove any rows from the result.

Try it yourself

Every part of this pipeline has been introduced above. Type it into the script, read each line aloud, and then run it:

practice_cars <- cars %>%
  filter(cyl != 8, gear %in% c(4, 5)) %>%
  select(mpg, cyl, gear)

practice_cars

12 Load the jobsatisfaction data

The Week 2 assignment uses a dataset included in the datarium package. Assign it to df:

df <- datarium::jobsatisfaction

Inspect the data before using it:

head(df)
names(df)
str(df)
[1] "id" "gender" "education_level" "score"

The dataset contains 58 observations and four variables:

Variable Type Meaning
id factor participant identifier
gender factor male or female
education_level factor school, college, or university
score numeric job-satisfaction score

Check the available factor levels before filtering or grouping:

levels(df$gender)
levels(df$education_level)

12.1 Remove unused factor levels after filtering

Filtering a data frame does not automatically remove factor levels that no longer appear. Use droplevels() when later output should show only the remaining categories:

university_example <- filter(
  df,
  education_level == "university"
)

university_example <- droplevels(university_example)
levels(university_example$education_level)
[1] "university"

13 Group work

The Week 2 practice is on a separate page so you can complete each task before seeing more worked code.

Open the Week 2 group work

Keep this lesson open in another tab and return to the examples whenever you need a code pattern.

14 Questions and troubleshooting

When a line does not work, bring these four pieces of information:

  1. The exact line of code you ran.
  2. The Console output or error message.
  3. What you expected the code to do.
  4. The object names, column names, quotation marks, commas, and parentheses you checked.

Use the remaining class time to complete the Week 2 group work and ask questions as they come up.

15 Week 2 reference

Task Code pattern
Convert a vector to a factor factor(x)
Set factor levels factor(x, levels = ...)
Replace codes with labels factor(x, levels = ..., labels = ...)
Mark a factor as ordered factor(x, ordered = TRUE)
Change the reference level relevel(x, ref = "...")
Install a package once install.packages("package")
Load a package each session library(package)
Use a specific package function package::function()
Calculate descriptive statistics psych::describe(x)
Describe by one group psych::describeBy(x, group = g)
Describe by two groups psych::describeBy(x, group = list(g1, g2))
Keep rows meeting a condition filter(data, condition)
Require several conditions filter(data, condition1, condition2)
Accept several values x %in% c(value1, value2)
Remove unused factor levels droplevels(data)
Send output to the next step data %>% function()
Keep selected columns select(data, column1, column2)
Calculate by group group_by() %>% summarize()